From d8a8565f163ee25040582635a18c0b8e0b2fd951 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 12 Jul 2021 16:36:22 +0200 Subject: [PATCH 0001/1104] Initial commit --- .gitignore | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 131 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b6e47617d --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# 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/ diff --git a/README.md b/README.md new file mode 100644 index 000000000..f7a14ae36 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# hdk +Homomorphic Development Framework - collection of tools to FHE all the things From ae2041202f5bad801e084febbac3ef6fdd4d2c25 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 12 Jul 2021 17:01:21 +0200 Subject: [PATCH 0002/1104] chore: add github issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 48 ++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/features.md | 17 ++++++++++ .github/ISSUE_TEMPLATE/refactor.md | 19 +++++++++++ 4 files changed, 85 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/features.md create mode 100644 .github/ISSUE_TEMPLATE/refactor.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..49d8c45a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug report +about: Create a reproducible bug report. +labels: bug, triage +--- + +## Summary + +What happened/what you expected to happen? + +## Description + +- versions affected: +- python version: +- config (optional: HW, OS): +- workaround (optional): if you’ve a way to workaround the issue +- proposed fix (optional): if you’ve a way to fix the issue + +Step by step procedure someone should follow to trigger the bug: + +
minimal POC to trigger the bug +

+ +```python +print("Minimal POC to reproduce the bug") +``` + +

+
+ +## Artifacts + +Generate a compiler report (see documentation) and attach all generated artifacts here: + +- bounds.txt +- cryptographic_parameters.txt +- ir_nodes.txt +- optimizations_applied.txt +- target_nodes.txt + +
Logs or output +

+ +```console +``` + +

+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..0086358db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/features.md b/.github/ISSUE_TEMPLATE/features.md new file mode 100644 index 000000000..85f57d9d2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/features.md @@ -0,0 +1,17 @@ +--- +name: Feature +about: Suggest a new feature. +labels: feature +--- + +## Summary + +Concise summary of the feature to be added. With a usecase example if applicable. + +## Problem to solve + +What are you trying to achieve that is not possible in the current features set. + +## Proposals + +What would be your proposals to implement it. diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md new file mode 100644 index 000000000..b5023e9f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.md @@ -0,0 +1,19 @@ +--- +name: Refactor +about: Suggest a code refactor. +labels: refactor +--- + +## Proposal + +What would be your refactor proposal + +## Impact + +List all files/modules/projects impacted by this refactor + +
Files impacted +

+file.py +

+
\ No newline at end of file From 2365a34b2af8a74ae5e1182c19d8301af8bf0e8c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 10:55:55 +0200 Subject: [PATCH 0003/1104] chore(tooling): add .editorconfig --- .editorconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..c783b32ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# 4 space indentation +[*.py] +charset = utf-8 +indent_style = space +indent_size = 4 From 68a6244593402f7f94b93324b62267d6362c54b2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 11:00:42 +0200 Subject: [PATCH 0004/1104] docs(dev): add ARCHITECTURE.md and initial frontend flow --- README.md | 2 ++ docs/dev/ARCHITECTURE.md | 7 +++++++ docs/dev/resources/frontend_flow.svg | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 docs/dev/ARCHITECTURE.md create mode 100644 docs/dev/resources/frontend_flow.svg diff --git a/README.md b/README.md index f7a14ae36..d40e3f482 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # hdk Homomorphic Development Framework - collection of tools to FHE all the things + +Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md new file mode 100644 index 000000000..eec1deb1e --- /dev/null +++ b/docs/dev/ARCHITECTURE.md @@ -0,0 +1,7 @@ +# Architecture of the project + +This is currently very much WIP. + +## Frontend flow: + + diff --git a/docs/dev/resources/frontend_flow.svg b/docs/dev/resources/frontend_flow.svg new file mode 100644 index 000000000..4d7788693 --- /dev/null +++ b/docs/dev/resources/frontend_flow.svg @@ -0,0 +1,3 @@ + + +
Input Program
v0: numpy
Input Program...
Tracing
Tracing
Data
Data
Algorithm/Function/Transform
Algorithm/Function/Transform
Operator DAG:
"Base Graph"
Operator DAG:...
Topological transform
Topological transform
Operator DAG:
"Candidate Graph"
Operator DAG:...
Conformance check
Conformance check
Constraints
Constraints
Input/Intermediate/Output values: 7b unsigned int
Constants: 7+1 = 8b signless int
Input/Intermediate/Output values: 7b unsigned int...
UI/UX
UI/UX
Error Message + Debug Infos
Error Message + Debug Infos
NO
NO
Bounds Measurement
Bounds Measurement
Dataset + Evaluation
Dataset + Evaluation
Input Bounds + Propagation
Input Bounds + Propagation
YES
YES
OR
OR
Data Widths
Data Widths
Opset v0: ADD, MUL, DOT, TLU
Opset v0: ADD, MUL, DOT, TLU
TLU Instantiation
TLU Instantiation
Operator DAG + Width:
"Compilable Graph"
Operator DAG + Width:...
MLIR Lowering
MLIR Lowering
MLIR
MLIR
Compiler "Backend"
Compiler "Backend"
Viewer does not support full SVG 1.1
\ No newline at end of file From 8c61a1258180915c7560896b28469ead1890b689 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 11:08:54 +0200 Subject: [PATCH 0005/1104] chore(tools): setup poetry and bare hdk package refs #15 --- hdk/__init__.py | 0 poetry.lock | 8 ++++++++ poetry.toml | 2 ++ pyproject.toml | 14 ++++++++++++++ 4 files changed, 24 insertions(+) create mode 100644 hdk/__init__.py create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml diff --git a/hdk/__init__.py b/hdk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..33d1cc45a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,8 @@ +package = [] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.6.2,<3.10" +content-hash = "2d9e90a917497268fdbfb614ad2daab88e4cfb12551d16cead793faff79aa4e5" + +[metadata.files] diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 000000000..ab1033bd3 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..bfc1071ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "hdk" +version = "0.1.0" +description = "Zama Homomorphic Development frameworK" +authors = ["Arthur Meyre "] + +[tool.poetry.dependencies] +python = ">=3.6.2,<3.10" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 58e35136f5f87b65d3fe46ddaa73fcb6df6e7836 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 11:46:19 +0200 Subject: [PATCH 0006/1104] chore(tools): add Makefile, formatting script, pylintrc and dependencies refs #15 --- Makefile | 27 ++ poetry.lock | 370 ++++++++++++++- pylintrc | 623 ++++++++++++++++++++++++++ pyproject.toml | 3 + script/source_format/format_python.sh | 48 ++ 5 files changed, 1069 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 pylintrc create mode 100644 script/source_format/format_python.sh diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..8face2371 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +setup_env: + poetry install + poetry run python -m pip install -U pip wheel setuptools +.PHONY: setup_env + +sync_env: + poetry install --remove-untracked + make setup_env +.PHONY: sync_env + +python_format: + poetry run env bash ./script/source_format/format_python.sh --dir hdk +.PHONY: python_format + +check_python_format: + poetry run env bash ./script/source_format/format_python.sh --dir hdk --check +.PHONY: check_python_format + +pylint: + poetry run pylint --rcfile=pylintrc hdk +.PHONY: pylint + +conformance: python_format +.PHONY: conformance + +pcc: check_python_format pylint +.PHONY: pcc diff --git a/poetry.lock b/poetry.lock index 33d1cc45a..d154bdfac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,8 +1,374 @@ -package = [] +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "astroid" +version = "2.6.2" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} +wrapt = ">=1.11,<1.13" + +[[package]] +name = "black" +version = "21.6b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} +mypy-extensions = ">=0.4.3" +pathspec = ">=0.8.1,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +python2 = ["typed-ast (>=1.4.2)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "dev" +optional = false +python-versions = ">=3.6, <3.7" + +[[package]] +name = "importlib-metadata" +version = "4.6.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "isort" +version = "5.9.2" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "lazy-object-proxy" +version = "1.6.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pylint" +version = "2.9.3" +description = "python code static checker" +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +astroid = ">=2.6.2,<2.7" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +toml = ">=0.7.1" + +[[package]] +name = "regex" +version = "2021.7.6" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "wrapt" +version = "1.12.1" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.5.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = ">=3.6.2,<3.10" -content-hash = "2d9e90a917497268fdbfb614ad2daab88e4cfb12551d16cead793faff79aa4e5" +content-hash = "8febd798f3e3dbf53d61a0c7870da8f7ff631e35b6d594c9bf1ab0d48ca4d400" [metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +astroid = [ + {file = "astroid-2.6.2-py3-none-any.whl", hash = "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"}, + {file = "astroid-2.6.2.tar.gz", hash = "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892"}, +] +black = [ + {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, + {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, + {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, +] +isort = [ + {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, + {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] +pylint = [ + {file = "pylint-2.9.3-py3-none-any.whl", hash = "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc"}, + {file = "pylint-2.9.3.tar.gz", hash = "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a"}, +] +regex = [ + {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, + {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, + {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, + {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, + {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, + {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, + {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, + {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, + {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, + {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, + {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, + {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, + {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +wrapt = [ + {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, +] +zipp = [ + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, +] diff --git a/pylintrc b/pylintrc new file mode 100644 index 000000000..f55239850 --- /dev/null +++ b/pylintrc @@ -0,0 +1,623 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Ignore function signatures when computing similarities. +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/pyproject.toml b/pyproject.toml index bfc1071ae..ce74a0be6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,9 @@ authors = ["Arthur Meyre "] python = ">=3.6.2,<3.10" [tool.poetry.dev-dependencies] +isort = "^5.9.2" +black = "21.6b0" +pylint = "^2.9.3" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/script/source_format/format_python.sh b/script/source_format/format_python.sh new file mode 100644 index 000000000..3ef6c090c --- /dev/null +++ b/script/source_format/format_python.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +function usage() { + echo "$0: install system and data, to support compiler" + echo + echo "--help Print this message" + echo "--check Do not apply format" + echo "--dir Specify a source directory" + echo +} + +CHECK= + +while [ -n "$1" ] +do + case $1 in + "--help" | "-h" ) + usage + exit 0 + ;; + + "--check" ) + CHECK="$1" + ;; + + "--dir" ) + shift + DIRS+=("$1") + ;; + + *) + echo "Unknown param : $1" + exit -1 + ;; + esac + shift +done + +for SRC_DIR in "${DIRS[@]}"; do + isort --profile black ${CHECK} ${SRC_DIR} + ((FAILURES+=$?)) + black -l 100 ${CHECK} ${SRC_DIR} + ((FAILURES+=$?)) +done + +if [[ "$FAILURES" != "0" ]]; then + exit 1 +fi From e45cd6cc86b3a2f30c67edc8bf8416142763f107 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 14:56:53 +0200 Subject: [PATCH 0007/1104] build: add github actions workflow for PR checks refs #15 --- .github/workflows/continuous-integration.yaml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/continuous-integration.yaml diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml new file mode 100644 index 000000000..85744f62d --- /dev/null +++ b/.github/workflows/continuous-integration.yaml @@ -0,0 +1,55 @@ +name: hdk PR checks + +on: [pull_request] + +jobs: + build: + concurrency: + group: ${{ github.head_ref }} + cancel-in-progress: true + + runs-on: ubuntu-20.04 + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Cache Installation Files + uses: actions/cache@v2 + with: + # Paths are Unix specific for now + path: | + ~/.cache/pip + ~/.cache/pypoetry + # Ignore line break in the evaluated double quoted string + key: "${{ runner.os }}-build-${{ matrix.python-version }}-\ + ${{ hashFiles('poetry.lock') }}" + restore-keys: | + ${{ runner.os }}-build-${{ matrix.python-version }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + make setup_env + - name: Conformance + id: conformance + if: ${{ success() && !cancelled() }} + run: | + make pcc + - name: Slack Notification + if: ${{ always() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: hdk-updates + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: 'Build finished with status ${{ job.status }}' + SLACK_USERNAME: zama-bot + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From ae992e4cdcbe48607203f0b6db22a4577b7abe57 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Thu, 15 Jul 2021 15:19:21 +0200 Subject: [PATCH 0008/1104] docs: move to sphinx relates #15 --- .github/workflows/continuous-integration.yaml | 10 + Makefile | 4 + docs/Makefile | 20 + docs/_static/css/zama.css | 35 ++ .../resources => _static}/frontend_flow.svg | 0 docs/conf.py | 68 +++ docs/dev/ARCHITECTURE.md | 4 +- docs/index.rst | 9 + docs/logo-black.png | Bin 0 -> 11502 bytes docs/make.bat | 35 ++ poetry.lock | 536 +++++++++++++++++- pyproject.toml | 3 + 12 files changed, 719 insertions(+), 5 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/_static/css/zama.css rename docs/{dev/resources => _static}/frontend_flow.svg (100%) create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/logo-black.png create mode 100644 docs/make.bat diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 85744f62d..41da8cae1 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -43,6 +43,16 @@ jobs: if: ${{ success() && !cancelled() }} run: | make pcc + - name: Build docs + id: docs + if: ${{ success() && !cancelled() }} + run: | + make docs + - name: Archive docs artifacts + uses: actions/upload-artifact@v2 + with: + name: html-docs + path: docs/_build/html - name: Slack Notification if: ${{ always() }} uses: rtCamp/action-slack-notify@v2 diff --git a/Makefile b/Makefile index 8face2371..085fba9ac 100644 --- a/Makefile +++ b/Makefile @@ -25,3 +25,7 @@ conformance: python_format pcc: check_python_format pylint .PHONY: pcc + +docs: + cd docs && poetry run make html +.PHONY: docs diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..d4bb2cbb9 --- /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 = . +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/_static/css/zama.css b/docs/_static/css/zama.css new file mode 100644 index 000000000..03162bef8 --- /dev/null +++ b/docs/_static/css/zama.css @@ -0,0 +1,35 @@ +/** css/zama.css **/ + +/* This line is theme specific - it includes the base theme CSS */ +@import 'theme.css'; /* for the Read the Docs theme */ + +.wy-side-nav-search { + background-color: #ffd208; +} + +.wy-menu-vertical header, .wy-menu-vertical p.caption { + color: #ffd208; +} + +.wy-side-nav-search > a { + position: relative; + padding-top: 50px; + color: black; +} + +.wy-side-nav-search > a::before { + display: none; +} + +.wy-side-nav-search > a .logo { + position: absolute; + top: 0; + left: 50%; + padding: 0; + margin: 0 0 0 -100px !important; +} + +.rst-content code.literal, .rst-content tt.literal { + color: #ffd208; + background-color: #696969; +} diff --git a/docs/dev/resources/frontend_flow.svg b/docs/_static/frontend_flow.svg similarity index 100% rename from docs/dev/resources/frontend_flow.svg rename to docs/_static/frontend_flow.svg diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..bc2acb827 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,68 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'Homomorphic Development Kit' +copyright = '2021, Zama' +author = 'Zama' + +# The full version, including alpha/beta/rc tags +release = '0.1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'myst_parser' +] + +myst_enable_extensions = [ + "amsmath", + "colon_fence", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' +html_style = 'css/zama.css' +html_logo = 'logo-black.png' +html_theme_options = { + 'logo_only': False, + 'display_version': True, +} +pygments_style = "zenburn" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index eec1deb1e..41eb9f15e 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -2,6 +2,6 @@ This is currently very much WIP. -## Frontend flow: +## Frontend flow - +![Frontend Flow](../_static/frontend_flow.svg) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..7a66c24a8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,9 @@ +Homomorphic Development Kit's documentation +=========================================== + + +.. toctree:: + :maxdepth: 2 + :caption: Developer docs + + dev/ARCHITECTURE.md \ No newline at end of file diff --git a/docs/logo-black.png b/docs/logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..c9ac1cdbdec61dfa25d0f4964fa0d4751ab16c56 GIT binary patch literal 11502 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 literal 0 HcmV?d00001 diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 000000000..2119f5109 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/poetry.lock b/poetry.lock index d154bdfac..1293cb2fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "appdirs" version = "1.4.4" @@ -20,6 +28,31 @@ typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpyth typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "babel" +version = "2.9.1" +description = "Internationalization utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytz = ">=2015.7" + [[package]] name = "black" version = "21.6b0" @@ -45,6 +78,25 @@ d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + [[package]] name = "click" version = "8.0.1" @@ -61,7 +113,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -73,6 +125,30 @@ category = "dev" optional = false python-versions = ">=3.6, <3.7" +[[package]] +name = "docutils" +version = "0.16" +description = "Docutils -- Python Documentation Utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "idna" +version = "3.2" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "imagesize" +version = "1.2.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "importlib-metadata" version = "4.6.1" @@ -104,6 +180,20 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "jinja2" +version = "3.0.1" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "lazy-object-proxy" version = "1.6.0" @@ -112,6 +202,34 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +[[package]] +name = "markdown-it-py" +version = "1.1.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +attrs = ">=19,<22" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +code_style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.2.2,<3.3.0)", "mistletoe-ebp (>=0.10.0,<0.11.0)", "mistune (>=0.8.4,<0.9.0)", "panflute (>=1.12,<2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +plugins = ["mdit-py-plugins"] +rtd = ["myst-nb (==0.13.0a1)", "pyyaml", "sphinx (>=2,<4)", "sphinx-copybutton", "sphinx-panels (>=0.4.0,<0.5.0)", "sphinx-book-theme"] +testing = ["coverage", "psutil", "pytest (>=3.6,<4)", "pytest-benchmark (>=3.2,<4.0)", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "mccabe" version = "0.6.1" @@ -120,6 +238,22 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mdit-py-plugins" +version = "0.2.8" +description = "Collection of plugins for markdown-it-py" +category = "main" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +markdown-it-py = ">=1.0,<2.0" + +[package.extras] +code_style = ["pre-commit (==2.6)"] +rtd = ["myst-parser (==0.14.0a3)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +testing = ["coverage", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"] + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -128,6 +262,39 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "myst-parser" +version = "0.15.1" +description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +docutils = ">=0.15,<0.18" +jinja2 = "*" +markdown-it-py = ">=1.0.0,<2.0.0" +mdit-py-plugins = ">=0.2.8,<0.3.0" +pyyaml = "*" +sphinx = ">=3,<5" + +[package.extras] +code_style = ["pre-commit (>=2.12,<3.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +rtd = ["ipython", "sphinx-book-theme (>=0.1.0,<0.2.0)", "sphinx-panels (>=0.5.2,<0.6.0)", "sphinxcontrib-bibtex (>=2.1,<3.0)", "sphinxext-rediraffe (>=0.2,<1.0)", "sphinxcontrib.mermaid (>=0.6.3,<0.7.0)", "sphinxext-opengraph (>=0.4.2,<0.5.0)"] +testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "packaging" +version = "21.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + [[package]] name = "pathspec" version = "0.8.1" @@ -136,6 +303,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pygments" +version = "2.9.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "pylint" version = "2.9.3" @@ -151,6 +326,30 @@ isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" toml = ">=0.7.1" +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + [[package]] name = "regex" version = "2021.7.6" @@ -159,6 +358,149 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "snowballstemmer" +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "sphinx" +version = "4.1.1" +description = "Python documentation generator" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.18" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] + +[[package]] +name = "sphinx-rtd-theme" +version = "0.5.2" +description = "Read the Docs theme for Sphinx" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +docutils = "<0.17" +sphinx = "*" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + [[package]] name = "toml" version = "0.10.2" @@ -179,10 +521,23 @@ python-versions = "*" name = "typing-extensions" version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" +category = "main" optional = false python-versions = "*" +[[package]] +name = "urllib3" +version = "1.26.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "wrapt" version = "1.12.1" @@ -206,9 +561,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.6.2,<3.10" -content-hash = "8febd798f3e3dbf53d61a0c7870da8f7ff631e35b6d594c9bf1ab0d48ca4d400" +content-hash = "f88bea4af82c1e7ad7b565aea809f650e9ddcd85e931b2377715f62ab26d99af" [metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -217,10 +576,26 @@ astroid = [ {file = "astroid-2.6.2-py3-none-any.whl", hash = "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"}, {file = "astroid-2.6.2.tar.gz", hash = "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892"}, ] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +babel = [ + {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, + {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, +] black = [ {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, ] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.2.tar.gz", hash = "sha256:951567c2f7433a70ab63f1be67e5ee05d3925d9423306ecb71a3b272757bcc95"}, + {file = "charset_normalizer-2.0.2-py3-none-any.whl", hash = "sha256:3c502a35807c9df35697b0f44b1d65008f83071ff29c69677c7c22573bc5a45a"}, +] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, @@ -233,6 +608,18 @@ dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +idna = [ + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, +] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] importlib-metadata = [ {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, @@ -241,6 +628,10 @@ isort = [ {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, ] +jinja2 = [ + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, +] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, @@ -265,22 +656,117 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, ] +markdown-it-py = [ + {file = "markdown-it-py-1.1.0.tar.gz", hash = "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3"}, + {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mdit-py-plugins = [ + {file = "mdit-py-plugins-0.2.8.tar.gz", hash = "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f"}, + {file = "mdit_py_plugins-0.2.8-py3-none-any.whl", hash = "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +myst-parser = [ + {file = "myst-parser-0.15.1.tar.gz", hash = "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603"}, + {file = "myst_parser-0.15.1-py3-none-any.whl", hash = "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"}, +] +packaging = [ + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] +pygments = [ + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, +] pylint = [ {file = "pylint-2.9.3-py3-none-any.whl", hash = "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc"}, {file = "pylint-2.9.3.tar.gz", hash = "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a"}, ] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] regex = [ {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, @@ -324,6 +810,46 @@ regex = [ {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, ] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, +] +sphinx = [ + {file = "Sphinx-4.1.1-py3-none-any.whl", hash = "sha256:3d513088236eef51e5b0adb78b0492eb22cc3b8ccdb0b36dd021173b365d4454"}, + {file = "Sphinx-4.1.1.tar.gz", hash = "sha256:23c846a1841af998cb736218539bb86d16f5eb95f5760b1966abcd2d584e62b8"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, + {file = "sphinx_rtd_theme-0.5.2.tar.gz", hash = "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -365,6 +891,10 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +urllib3 = [ + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, +] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] diff --git a/pyproject.toml b/pyproject.toml index ce74a0be6..fc5883609 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,9 @@ authors = ["Arthur Meyre "] [tool.poetry.dependencies] python = ">=3.6.2,<3.10" +Sphinx = "^4.1.1" +sphinx-rtd-theme = "^0.5.2" +myst-parser = "^0.15.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" From a2185af578cd4bf1fa90542611a82f63ec6f6171 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 18:06:46 +0200 Subject: [PATCH 0009/1104] tests: add test structure and dependencies - add unique ID generator to hdk and unit test it refs #15 --- Makefile | 6 +- hdk/__init__.py | 2 + hdk/utils/__init__.py | 3 + hdk/utils/misc.py | 17 +++ poetry.lock | 230 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 3 + tests/utils/test_misc.py | 17 +++ 7 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 hdk/utils/__init__.py create mode 100644 hdk/utils/misc.py create mode 100644 tests/utils/test_misc.py diff --git a/Makefile b/Makefile index 085fba9ac..1ddc0260e 100644 --- a/Makefile +++ b/Makefile @@ -9,15 +9,15 @@ sync_env: .PHONY: sync_env python_format: - poetry run env bash ./script/source_format/format_python.sh --dir hdk + poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests .PHONY: python_format check_python_format: - poetry run env bash ./script/source_format/format_python.sh --dir hdk --check + poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --check .PHONY: check_python_format pylint: - poetry run pylint --rcfile=pylintrc hdk + poetry run pylint --rcfile=pylintrc hdk tests .PHONY: pylint conformance: python_format diff --git a/hdk/__init__.py b/hdk/__init__.py index e69de29bb..1df85bfe7 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -0,0 +1,2 @@ +"""HDK's top import""" +from . import utils diff --git a/hdk/utils/__init__.py b/hdk/utils/__init__.py new file mode 100644 index 000000000..314ca624d --- /dev/null +++ b/hdk/utils/__init__.py @@ -0,0 +1,3 @@ +"""HDK's utils module""" +from . import misc +from .misc import * diff --git a/hdk/utils/misc.py b/hdk/utils/misc.py new file mode 100644 index 000000000..98d67b1b8 --- /dev/null +++ b/hdk/utils/misc.py @@ -0,0 +1,17 @@ +"""Misc. utils for hdk""" + + +def get_unique_id(): + """Function to get a unique ID""" + + if not hasattr(get_unique_id, "generator"): + + def generator(): + current_id = 0 + while True: + yield current_id + current_id += 1 + + get_unique_id.generator = generator() + + return next(get_unique_id.generator) diff --git a/poetry.lock b/poetry.lock index 1293cb2fb..69e4d6c93 100644 --- a/poetry.lock +++ b/poetry.lock @@ -28,6 +28,14 @@ typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpyth typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "attrs" version = "21.2.0" @@ -86,6 +94,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "charset-normalizer" version = "2.0.2" @@ -117,6 +133,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + [[package]] name = "dataclasses" version = "0.8" @@ -125,6 +152,21 @@ category = "dev" optional = false python-versions = ">=3.6, <3.7" +[[package]] +name = "diff-cover" +version = "6.2.0" +description = "Automatically find diff lines that need test coverage." +category = "dev" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +chardet = ">=3.0.0" +Jinja2 = ">=2.7.1" +jinja2-pluralize = "*" +pluggy = "*" +pygments = "*" + [[package]] name = "docutils" version = "0.16" @@ -166,6 +208,26 @@ docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +[[package]] +name = "inflect" +version = "5.3.0" +description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pygments", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "isort" version = "5.9.2" @@ -194,6 +256,18 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jinja2-pluralize" +version = "0.3.0" +description = "Jinja2 pluralize filters." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +inflect = ">=0.2.4" +jinja2 = ">=2.4" + [[package]] name = "lazy-object-proxy" version = "1.6.0" @@ -303,6 +377,28 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pygments" version = "2.9.0" @@ -334,6 +430,44 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + [[package]] name = "pytz" version = "2021.1" @@ -561,7 +695,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.6.2,<3.10" -content-hash = "f88bea4af82c1e7ad7b565aea809f650e9ddcd85e931b2377715f62ab26d99af" +content-hash = "4ebc7aa86f99c8bd6424a20fe54a17991f7084444693669731855a53e091d355" [metadata.files] alabaster = [ @@ -576,6 +710,10 @@ astroid = [ {file = "astroid-2.6.2-py3-none-any.whl", hash = "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"}, {file = "astroid-2.6.2.tar.gz", hash = "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892"}, ] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, @@ -592,6 +730,10 @@ certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] charset-normalizer = [ {file = "charset-normalizer-2.0.2.tar.gz", hash = "sha256:951567c2f7433a70ab63f1be67e5ee05d3925d9423306ecb71a3b272757bcc95"}, {file = "charset_normalizer-2.0.2-py3-none-any.whl", hash = "sha256:3c502a35807c9df35697b0f44b1d65008f83071ff29c69677c7c22573bc5a45a"}, @@ -604,10 +746,68 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] +diff-cover = [ + {file = "diff_cover-6.2.0-py3-none-any.whl", hash = "sha256:c2d5c6f6ec8dceddabc4abcefc984e937cd7e5f14787968c991c57f1e2b13c03"}, + {file = "diff_cover-6.2.0.tar.gz", hash = "sha256:66a20cb0f0a631792849af5f1b5e2dff254937c24983a4b63b8d483e0e9cf7a6"}, +] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, @@ -624,6 +824,14 @@ importlib-metadata = [ {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, ] +inflect = [ + {file = "inflect-5.3.0-py3-none-any.whl", hash = "sha256:42560be16af702a21d43d59427f276b5aed79efb1ded9b713468c081f4353d10"}, + {file = "inflect-5.3.0.tar.gz", hash = "sha256:41a23f6788962e9775e40e2ecfb1d6455d02de315022afeedd3c5dc070019d73"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] isort = [ {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, @@ -632,6 +840,10 @@ jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] +jinja2-pluralize = [ + {file = "jinja2_pluralize-0.3.0-py2.py3-none-any.whl", hash = "sha256:4fec874a591014774d4c66cb7f65314390731bfc57db4c27119db61aa93b2bc4"}, + {file = "jinja2_pluralize-0.3.0.tar.gz", hash = "sha256:df5c2d5017b9b54c0a66cb790cca9fc08945837c3dbfc323589203f1ffb73c1c"}, +] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, @@ -720,6 +932,14 @@ pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] pygments = [ {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, @@ -732,6 +952,14 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, diff --git a/pyproject.toml b/pyproject.toml index fc5883609..24139ba94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,9 @@ myst-parser = "^0.15.1" isort = "^5.9.2" black = "21.6b0" pylint = "^2.9.3" +pytest = "^6.2.4" +pytest-cov = "^2.12.1" +diff-cover = "^6.2.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/utils/test_misc.py b/tests/utils/test_misc.py new file mode 100644 index 000000000..e7438adbd --- /dev/null +++ b/tests/utils/test_misc.py @@ -0,0 +1,17 @@ +"""Test file for HDK's misc utils""" +import random + +import hdk + + +def test_get_unique_id(): + """Test get_unique_id""" + how_many_ids = random.randint(2, 100) + generated_ids = [hdk.utils.get_unique_id() for __ in range(how_many_ids)] + + len_generated_ids = len(generated_ids) + len_unique_ids = len(set(generated_ids)) + + assert ( + len_generated_ids == len_unique_ids + ), f"Expected to have uniques ids, generated {len_generated_ids}, only had {len_unique_ids}" From 7dccfa649c62bc2c1316f9988cb5ec2d5af8edb0 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 15 Jul 2021 18:57:28 +0200 Subject: [PATCH 0010/1104] build(tests): add tests and coverage to CI workflow #refs 15 --- .github/workflows/continuous-integration.yaml | 19 ++++++++++++++++++- .gitignore | 1 + Makefile | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 41da8cae1..fb72d470e 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -15,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -43,12 +45,27 @@ jobs: if: ${{ success() && !cancelled() }} run: | make pcc + - name: PyTest + if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + run: | + make pytest + - name: Test coverage + if: ${{ success() && !cancelled() }} + run: | + poetry run diff-cover coverage.xml --fail-under 100 --html-report coverage.html --compare-branch origin/${{ github.base_ref }} + - name: Archive test coverage + uses: actions/upload-artifact@v2 + if: ${{ success() && !cancelled() }} + with: + name: coverage + path: coverage.html - name: Build docs id: docs - if: ${{ success() && !cancelled() }} + if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} run: | make docs - name: Archive docs artifacts + if: ${{ steps.docs.outcome == 'success' && !cancelled() }} uses: actions/upload-artifact@v2 with: name: html-docs diff --git a/.gitignore b/.gitignore index b6e47617d..e62d491d2 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +coverage.html *.cover *.py,cover .hypothesis/ diff --git a/Makefile b/Makefile index 1ddc0260e..1746eb6a9 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,10 @@ conformance: python_format pcc: check_python_format pylint .PHONY: pcc +pytest: + poetry run pytest --cov=hdk -vv --cov-report=xml tests/ +.PHONY: pytest + docs: cd docs && poetry run make html .PHONY: docs From ef78e66241007690e45719237099c357be3933b2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 16 Jul 2021 10:05:19 +0200 Subject: [PATCH 0011/1104] build(coverage): add comment to PR with diff-cover output - add script to help format the coverage comment - manage steps logic refs #15 --- .github/workflows/continuous-integration.yaml | 15 +++++-- script/actions_utils/coverage.sh | 19 ++++++++ .../actions_utils/coverage_report_format.py | 45 +++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 script/actions_utils/coverage.sh create mode 100755 script/actions_utils/coverage_report_format.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index fb72d470e..fb9f5b77d 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -46,19 +46,28 @@ jobs: run: | make pcc - name: PyTest + id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} run: | make pytest - name: Test coverage - if: ${{ success() && !cancelled() }} + id: coverage + if: ${{ steps.pytest.outcome != 'skipped' && !cancelled() }} run: | - poetry run diff-cover coverage.xml --fail-under 100 --html-report coverage.html --compare-branch origin/${{ github.base_ref }} + bash --noprofile --norc -o pipefail \ + script/actions_utils/coverage.sh ${{ github.base_ref }} - name: Archive test coverage uses: actions/upload-artifact@v2 - if: ${{ success() && !cancelled() }} + if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} with: name: coverage path: coverage.html + - name: Comment with coverage + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} + with: + path: diff-coverage.txt + recreate: true - name: Build docs id: docs if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} diff --git a/script/actions_utils/coverage.sh b/script/actions_utils/coverage.sh new file mode 100644 index 000000000..1882ea005 --- /dev/null +++ b/script/actions_utils/coverage.sh @@ -0,0 +1,19 @@ +CURR_DIR=`dirname $0` + +# Run diff-coverage +poetry run diff-cover coverage.xml --fail-under 100 \ +--html-report coverage.html \ +--compare-branch origin/"$1" | tee diff-coverage.txt + +# Get exit code without closing the script +TEST_EXIT_CODE="$?" + +# Format diff-coverage.txt for PR comment +poetry run python script/actions_utils/coverage_report_format.py \ +--diff-cover-exit-code "$TEST_EXIT_CODE" \ +--diff-cover-output diff-coverage.txt + +# Set exit code if test failed +if [[ "$TEST_EXIT_CODE" != "0" ]]; then + exit "$TEST_EXIT_CODE" +fi diff --git a/script/actions_utils/coverage_report_format.py b/script/actions_utils/coverage_report_format.py new file mode 100755 index 000000000..0d8340529 --- /dev/null +++ b/script/actions_utils/coverage_report_format.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +"""Helper script for github actions""" +import argparse +import traceback +from pathlib import Path + + +def main(args): + """Entry point""" + diff_cover_file_path = Path(args.diff_cover_output).resolve().absolute() + + diff_cover_content = None + + with open(diff_cover_file_path, "r") as f: + diff_cover_content = f.readlines() + + with open(diff_cover_file_path, "w", encoding="utf-8") as f: + if args.diff_cover_exit_code == 0: + f.write("## Coverage passed ✅\n\n") + else: + f.write("## Coverage failed ❌\n\n") + + # Open collapsible section + f.write("
Coverage details\n

\n\n") + f.write("```\n") + + f.writelines(diff_cover_content) + + # Close collapsible section + f.write("```\n\n") + f.write("

\n
\n\n") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(allow_abbrev=False) + + parser.add_argument("--diff-cover-exit-code", type=int, required=True) + parser.add_argument("--diff-cover-output", type=str, required=True) + + cli_args = parser.parse_args() + try: + main(cli_args) + except Exception as e: + traceback.print_exc() From e0c838b23086aa4af4d4591cc6a6607f4055dbfd Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 16 Jul 2021 11:50:46 +0200 Subject: [PATCH 0012/1104] chore(tools): a less annoying pylint - allow 1 letter names - no minimum on class public methods --- pylintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylintrc b/pylintrc index f55239850..07f7a7a29 100644 --- a/pylintrc +++ b/pylintrc @@ -275,7 +275,7 @@ good-names=i, # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted -good-names-rgxs= +good-names-rgxs=^[a-z]$ # Include a hint for the correct naming format with invalid-name. include-naming-hint=no @@ -571,7 +571,7 @@ max-returns=6 max-statements=50 # Minimum number of public methods for a class (see R0903). -min-public-methods=2 +min-public-methods=0 [IMPORTS] From 36d93a60d997d767d400e22b517f0a66b65c05af Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 16 Jul 2021 12:30:06 +0200 Subject: [PATCH 0013/1104] chore(tools): add mypy as dev tool but don't check in CI for now - we'll see if the benefits of static typing are worth it or not --- .github/workflows/continuous-integration.yaml | 4 +- Makefile | 6 ++- hdk/utils/misc.py | 9 ++-- poetry.lock | 45 ++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index fb9f5b77d..3962ba589 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -43,8 +43,10 @@ jobs: - name: Conformance id: conformance if: ${{ success() && !cancelled() }} + # keep going register errors in the intermediate target but executes them all + # Nicer to have pcc complete for the dev and have all the relevan conformance issues run: | - make pcc + make --keep-going pcc - name: PyTest id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} diff --git a/Makefile b/Makefile index 1746eb6a9..02e5a8349 100644 --- a/Makefile +++ b/Makefile @@ -23,13 +23,17 @@ pylint: conformance: python_format .PHONY: conformance -pcc: check_python_format pylint +pcc: check_python_format pylint mypy .PHONY: pcc pytest: poetry run pytest --cov=hdk -vv --cov-report=xml tests/ .PHONY: pytest +mypy: + poetry run mypy -p hdk +.PHONY: mypy + docs: cd docs && poetry run make html .PHONY: docs diff --git a/hdk/utils/misc.py b/hdk/utils/misc.py index 98d67b1b8..197ea760c 100644 --- a/hdk/utils/misc.py +++ b/hdk/utils/misc.py @@ -1,17 +1,18 @@ """Misc. utils for hdk""" +from typing import Iterator -def get_unique_id(): +def get_unique_id() -> int: """Function to get a unique ID""" if not hasattr(get_unique_id, "generator"): - def generator(): + def generator() -> Iterator[int]: current_id = 0 while True: yield current_id current_id += 1 - get_unique_id.generator = generator() + setattr(get_unique_id, "generator", generator()) - return next(get_unique_id.generator) + return next(getattr(get_unique_id, "generator")) diff --git a/poetry.lock b/poetry.lock index 69e4d6c93..9858db152 100644 --- a/poetry.lock +++ b/poetry.lock @@ -328,6 +328,24 @@ code_style = ["pre-commit (==2.6)"] rtd = ["myst-parser (==0.14.0a3)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] testing = ["coverage", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"] +[[package]] +name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +toml = "*" +typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -695,7 +713,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.6.2,<3.10" -content-hash = "4ebc7aa86f99c8bd6424a20fe54a17991f7084444693669731855a53e091d355" +content-hash = "bc169b8367e097e585c48c7036341a65eec0c2f5cecc8fb873fe505e311791e2" [metadata.files] alabaster = [ @@ -916,6 +934,31 @@ mdit-py-plugins = [ {file = "mdit-py-plugins-0.2.8.tar.gz", hash = "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f"}, {file = "mdit_py_plugins-0.2.8-py3-none-any.whl", hash = "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c"}, ] +mypy = [ + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, diff --git a/pyproject.toml b/pyproject.toml index 24139ba94..7c0edfffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ pylint = "^2.9.3" pytest = "^6.2.4" pytest-cov = "^2.12.1" diff-cover = "^6.2.0" +mypy = "^0.910" [build-system] requires = ["poetry-core>=1.0.0"] From 9bf380bfce78dfd7d0b8736de0b0ff11bfd5a072 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 19 Jul 2021 16:32:53 +0200 Subject: [PATCH 0014/1104] fix(typo): fix unfortunate typos --- .github/workflows/continuous-integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 3962ba589..15e319507 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -43,8 +43,8 @@ jobs: - name: Conformance id: conformance if: ${{ success() && !cancelled() }} - # keep going register errors in the intermediate target but executes them all - # Nicer to have pcc complete for the dev and have all the relevan conformance issues + # keep going registers errors in the intermediate targets but executes them all + # Nicer to have pcc complete for the dev and have all the relevant conformance issues run: | make --keep-going pcc - name: PyTest From ac31a712cab39b909230792b744ba1b290e709fe Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 21 Jul 2021 13:47:45 +0200 Subject: [PATCH 0015/1104] chore(build): update a CI command to properly call pip --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 15e319507..f4a8e1efc 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -38,7 +38,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install poetry + python -m pip install poetry make setup_env - name: Conformance id: conformance From 2208a87327e3b24353576685db25efbff4cdb1b2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 21 Jul 2021 13:49:12 +0200 Subject: [PATCH 0016/1104] chore(git): ignore output file for diff-coverage --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e62d491d2..851e2ddea 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ htmlcov/ nosetests.xml coverage.xml coverage.html +diff-coverage.txt *.cover *.py,cover .hypothesis/ From 94f79f234503c4f6a21091888aeaadcdc24fd403 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 21 Jul 2021 13:52:10 +0200 Subject: [PATCH 0017/1104] chore(tools): update coverage.sh script --- .github/workflows/continuous-integration.yaml | 3 +-- script/actions_utils/coverage.sh | 13 ++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) mode change 100644 => 100755 script/actions_utils/coverage.sh diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index f4a8e1efc..4a35a7038 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -56,8 +56,7 @@ jobs: id: coverage if: ${{ steps.pytest.outcome != 'skipped' && !cancelled() }} run: | - bash --noprofile --norc -o pipefail \ - script/actions_utils/coverage.sh ${{ github.base_ref }} + ./script/actions_utils/coverage.sh ${{ github.base_ref }} - name: Archive test coverage uses: actions/upload-artifact@v2 if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} diff --git a/script/actions_utils/coverage.sh b/script/actions_utils/coverage.sh old mode 100644 new mode 100755 index 1882ea005..6389a4e64 --- a/script/actions_utils/coverage.sh +++ b/script/actions_utils/coverage.sh @@ -1,3 +1,8 @@ +#!/usr/bin/env bash + +set -o pipefail +set +e + CURR_DIR=`dirname $0` # Run diff-coverage @@ -9,11 +14,9 @@ poetry run diff-cover coverage.xml --fail-under 100 \ TEST_EXIT_CODE="$?" # Format diff-coverage.txt for PR comment -poetry run python script/actions_utils/coverage_report_format.py \ +poetry run python "$CURR_DIR"/coverage_report_format.py \ --diff-cover-exit-code "$TEST_EXIT_CODE" \ --diff-cover-output diff-coverage.txt -# Set exit code if test failed -if [[ "$TEST_EXIT_CODE" != "0" ]]; then - exit "$TEST_EXIT_CODE" -fi +# Set exit code to the diff coverage check +exit "$TEST_EXIT_CODE" From 42e9b8af2cc16804053a1710e25b5d697b54e32c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 16 Jul 2021 17:19:31 +0200 Subject: [PATCH 0018/1104] chore: change requirements to be able to use networkx for graph handling - support only Python >= 3.7, numpy is also dropping support for 3.6 --- poetry.lock | 310 +++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 3 +- 2 files changed, 301 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9858db152..c0eb2c77b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -72,7 +72,6 @@ python-versions = ">=3.6.2" [package.dependencies] appdirs = "*" click = ">=7.1.2" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.8.1,<1" regex = ">=2020.1.8" @@ -145,12 +144,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" toml = ["toml"] [[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -category = "dev" +name = "cycler" +version = "0.10.0" +description = "Composable style cycles" +category = "main" optional = false -python-versions = ">=3.6, <3.7" +python-versions = "*" + +[package.dependencies] +six = "*" [[package]] name = "diff-cover" @@ -268,6 +270,14 @@ python-versions = "*" inflect = ">=0.2.4" jinja2 = ">=2.4" +[[package]] +name = "kiwisolver" +version = "1.3.1" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "lazy-object-proxy" version = "1.6.0" @@ -304,6 +314,22 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "matplotlib" +version = "3.4.2" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cycler = ">=0.10" +kiwisolver = ">=1.0.1" +numpy = ">=1.16" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" + [[package]] name = "mccabe" version = "0.6.1" @@ -376,6 +402,34 @@ linkify = ["linkify-it-py (>=1.0,<2.0)"] rtd = ["ipython", "sphinx-book-theme (>=0.1.0,<0.2.0)", "sphinx-panels (>=0.5.2,<0.6.0)", "sphinxcontrib-bibtex (>=2.1,<3.0)", "sphinxext-rediraffe (>=0.2,<1.0)", "sphinxcontrib.mermaid (>=0.6.3,<0.7.0)", "sphinxext-opengraph (>=0.4.2,<0.5.0)"] testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"] +[[package]] +name = "networkx" +version = "2.6.1" +description = "Python package for creating and manipulating graphs and networks" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +matplotlib = ">=3.3" +numpy = ">=1.19" +pandas = ">=1.1" +scipy = ">=1.5,<1.6.1 || >1.6.1" + +[package.extras] +developer = ["black (==21.5b1)", "pre-commit (>=2.12)"] +doc = ["sphinx (>=4.0,<5.0)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx-gallery (>=0.9,<1.0)", "numpydoc (>=1.1)", "pillow (>=8.2)", "nb2plots (>=0.6)", "texext (>=0.6.6)"] +extra = ["lxml (>=4.5)", "pygraphviz (>=1.7)", "pydot (>=1.4.1)"] +test = ["pytest (>=6.2)", "pytest-cov (>=2.12)", "codecov (>=2.1)"] + +[[package]] +name = "numpy" +version = "1.21.0" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "packaging" version = "21.0" @@ -387,6 +441,22 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "pandas" +version = "1.1.5" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +numpy = ">=1.15.4" +python-dateutil = ">=2.7.3" +pytz = ">=2017.2" + +[package.extras] +test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"] + [[package]] name = "pathspec" version = "0.8.1" @@ -395,6 +465,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pillow" +version = "8.3.1" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "pluggy" version = "0.13.1" @@ -486,6 +564,17 @@ toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pytz" version = "2021.1" @@ -528,6 +617,25 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "scipy" +version = "1.7.0" +description = "SciPy: Scientific Library for Python" +category = "main" +optional = false +python-versions = ">=3.7,<3.10" + +[package.dependencies] +numpy = ">=1.16.5,<1.23.0" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "snowballstemmer" version = "2.1.0" @@ -712,8 +820,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" -python-versions = ">=3.6.2,<3.10" -content-hash = "bc169b8367e097e585c48c7036341a65eec0c2f5cecc8fb873fe505e311791e2" +python-versions = ">=3.7,<3.10" +content-hash = "2dbebb6e1d3b5f35cd48891bb9ed49b9d57d1f5659419e27150c5a3786d4b054" [metadata.files] alabaster = [ @@ -818,9 +926,9 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] -dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +cycler = [ + {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, + {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] diff-cover = [ {file = "diff_cover-6.2.0-py3-none-any.whl", hash = "sha256:c2d5c6f6ec8dceddabc4abcefc984e937cd7e5f14787968c991c57f1e2b13c03"}, @@ -862,6 +970,40 @@ jinja2-pluralize = [ {file = "jinja2_pluralize-0.3.0-py2.py3-none-any.whl", hash = "sha256:4fec874a591014774d4c66cb7f65314390731bfc57db4c27119db61aa93b2bc4"}, {file = "jinja2_pluralize-0.3.0.tar.gz", hash = "sha256:df5c2d5017b9b54c0a66cb790cca9fc08945837c3dbfc323589203f1ffb73c1c"}, ] +kiwisolver = [ + {file = "kiwisolver-1.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-win32.whl", hash = "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454"}, + {file = "kiwisolver-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18"}, + {file = "kiwisolver-1.3.1-cp38-cp38-win32.whl", hash = "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81"}, + {file = "kiwisolver-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e"}, + {file = "kiwisolver-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54"}, + {file = "kiwisolver-1.3.1-cp39-cp39-win32.whl", hash = "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030"}, + {file = "kiwisolver-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6"}, + {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d"}, + {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3"}, + {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6"}, + {file = "kiwisolver-1.3.1.tar.gz", hash = "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248"}, +] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, @@ -926,6 +1068,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] +matplotlib = [ + {file = "matplotlib-3.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c541ee5a3287efe066bbe358320853cf4916bc14c00c38f8f3d8d75275a405a9"}, + {file = "matplotlib-3.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3a5c18dbd2c7c366da26a4ad1462fe3e03a577b39e3b503bbcf482b9cdac093c"}, + {file = "matplotlib-3.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a9d8cb5329df13e0cdaa14b3b43f47b5e593ec637f13f14db75bb16e46178b05"}, + {file = "matplotlib-3.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:7ad19f3fb6145b9eb41c08e7cbb9f8e10b91291396bee21e9ce761bb78df63ec"}, + {file = "matplotlib-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:7a58f3d8fe8fac3be522c79d921c9b86e090a59637cb88e3bc51298d7a2c862a"}, + {file = "matplotlib-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6382bc6e2d7e481bcd977eb131c31dee96e0fb4f9177d15ec6fb976d3b9ace1a"}, + {file = "matplotlib-3.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a6a44f27aabe720ec4fd485061e8a35784c2b9ffa6363ad546316dfc9cea04e"}, + {file = "matplotlib-3.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1c1779f7ab7d8bdb7d4c605e6ffaa0614b3e80f1e3c8ccf7b9269a22dbc5986b"}, + {file = "matplotlib-3.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5826f56055b9b1c80fef82e326097e34dc4af8c7249226b7dd63095a686177d1"}, + {file = "matplotlib-3.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0bea5ec5c28d49020e5d7923c2725b837e60bc8be99d3164af410eb4b4c827da"}, + {file = "matplotlib-3.4.2-cp38-cp38-win32.whl", hash = "sha256:6475d0209024a77f869163ec3657c47fed35d9b6ed8bccba8aa0f0099fbbdaa8"}, + {file = "matplotlib-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:21b31057bbc5e75b08e70a43cefc4c0b2c2f1b1a850f4a0f7af044eb4163086c"}, + {file = "matplotlib-3.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b26535b9de85326e6958cdef720ecd10bcf74a3f4371bf9a7e5b2e659c17e153"}, + {file = "matplotlib-3.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:32fa638cc10886885d1ca3d409d4473d6a22f7ceecd11322150961a70fab66dd"}, + {file = "matplotlib-3.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:956c8849b134b4a343598305a3ca1bdd3094f01f5efc8afccdebeffe6b315247"}, + {file = "matplotlib-3.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:85f191bb03cb1a7b04b5c2cca4792bef94df06ef473bc49e2818105671766fee"}, + {file = "matplotlib-3.4.2-cp39-cp39-win32.whl", hash = "sha256:b1d5a2cedf5de05567c441b3a8c2651fbde56df08b82640e7f06c8cd91e201f6"}, + {file = "matplotlib-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:df815378a754a7edd4559f8c51fc7064f779a74013644a7f5ac7a0c31f875866"}, + {file = "matplotlib-3.4.2.tar.gz", hash = "sha256:d8d994cefdff9aaba45166eb3de4f5211adb4accac85cbf97137e98f26ea0219"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -967,14 +1130,110 @@ myst-parser = [ {file = "myst-parser-0.15.1.tar.gz", hash = "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603"}, {file = "myst_parser-0.15.1-py3-none-any.whl", hash = "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"}, ] +networkx = [ + {file = "networkx-2.6.1-py3-none-any.whl", hash = "sha256:aa21cd7c7e0696672885866b2736a32bfd4bd20996ab99fc6142edf9357d3241"}, + {file = "networkx-2.6.1.tar.gz", hash = "sha256:bf4cb807d1bccf1593c7d0742d9127d9e04e021867299082658b0fc3907924e8"}, +] +numpy = [ + {file = "numpy-1.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d5caa946a9f55511e76446e170bdad1d12d6b54e17a2afe7b189112ed4412bb8"}, + {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ac4fd578322842dbda8d968e3962e9f22e862b6ec6e3378e7415625915e2da4d"}, + {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:598fe100b2948465cf3ed64b1a326424b5e4be2670552066e17dfaa67246011d"}, + {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c55407f739f0bfcec67d0df49103f9333edc870061358ac8a8c9e37ea02fcd2"}, + {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75579acbadbf74e3afd1153da6177f846212ea2a0cc77de53523ae02c9256513"}, + {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc367c86eb87e5b7c9592935620f22d13b090c609f1b27e49600cd033b529f54"}, + {file = "numpy-1.21.0-cp37-cp37m-win32.whl", hash = "sha256:d89b0dc7f005090e32bb4f9bf796e1dcca6b52243caf1803fdd2b748d8561f63"}, + {file = "numpy-1.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eda2829af498946c59d8585a9fd74da3f810866e05f8df03a86f70079c7531dd"}, + {file = "numpy-1.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1a784e8ff7ea2a32e393cc53eb0003eca1597c7ca628227e34ce34eb11645a0e"}, + {file = "numpy-1.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bba474a87496d96e61461f7306fba2ebba127bed7836212c360f144d1e72ac54"}, + {file = "numpy-1.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd0a359c1c17f00cb37de2969984a74320970e0ceef4808c32e00773b06649d9"}, + {file = "numpy-1.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4d5a86a5257843a18fb1220c5f1c199532bc5d24e849ed4b0289fb59fbd4d8f"}, + {file = "numpy-1.21.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:620732f42259eb2c4642761bd324462a01cdd13dd111740ce3d344992dd8492f"}, + {file = "numpy-1.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9205711e5440954f861ceeea8f1b415d7dd15214add2e878b4d1cf2bcb1a914"}, + {file = "numpy-1.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ad09f55cc95ed8d80d8ab2052f78cc21cb231764de73e229140d81ff49d8145e"}, + {file = "numpy-1.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1f2fb2da242568af0271455b89aee0f71e4e032086ee2b4c5098945d0e11cf6"}, + {file = "numpy-1.21.0-cp38-cp38-win32.whl", hash = "sha256:e58ddb53a7b4959932f5582ac455ff90dcb05fac3f8dcc8079498d43afbbde6c"}, + {file = "numpy-1.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:d2910d0a075caed95de1a605df00ee03b599de5419d0b95d55342e9a33ad1fb3"}, + {file = "numpy-1.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a290989cd671cd0605e9c91a70e6df660f73ae87484218e8285c6522d29f6e38"}, + {file = "numpy-1.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3537b967b350ad17633b35c2f4b1a1bbd258c018910b518c30b48c8e41272717"}, + {file = "numpy-1.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc6c650f8700ce1e3a77668bb7c43e45c20ac06ae00d22bdf6760b38958c883"}, + {file = "numpy-1.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:709884863def34d72b183d074d8ba5cfe042bc3ff8898f1ffad0209161caaa99"}, + {file = "numpy-1.21.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bebab3eaf0641bba26039fb0b2c5bf9b99407924b53b1ea86e03c32c64ef5aef"}, + {file = "numpy-1.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf680682ad0a3bef56dae200dbcbac2d57294a73e5b0f9864955e7dd7c2c2491"}, + {file = "numpy-1.21.0-cp39-cp39-win32.whl", hash = "sha256:d95d16204cd51ff1a1c8d5f9958ce90ae190be81d348b514f9be39f878b8044a"}, + {file = "numpy-1.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:2ba579dde0563f47021dcd652253103d6fd66165b18011dce1a0609215b2791e"}, + {file = "numpy-1.21.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c40e6b860220ed862e8097b8f81c9af6d7405b723f4a7af24a267b46f90e461"}, + {file = "numpy-1.21.0.zip", hash = "sha256:e80fe25cba41c124d04c662f33f6364909b985f2eb5998aaa5ae4b9587242cce"}, +] packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] +pandas = [ + {file = "pandas-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d"}, + {file = "pandas-1.1.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca"}, + {file = "pandas-1.1.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814"}, + {file = "pandas-1.1.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f"}, + {file = "pandas-1.1.5-cp36-cp36m-win32.whl", hash = "sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5"}, + {file = "pandas-1.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648"}, + {file = "pandas-1.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a"}, + {file = "pandas-1.1.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086"}, + {file = "pandas-1.1.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae"}, + {file = "pandas-1.1.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788"}, + {file = "pandas-1.1.5-cp37-cp37m-win32.whl", hash = "sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb"}, + {file = "pandas-1.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98"}, + {file = "pandas-1.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11"}, + {file = "pandas-1.1.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9"}, + {file = "pandas-1.1.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e"}, + {file = "pandas-1.1.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b"}, + {file = "pandas-1.1.5-cp38-cp38-win32.whl", hash = "sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b"}, + {file = "pandas-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d"}, + {file = "pandas-1.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a"}, + {file = "pandas-1.1.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a"}, + {file = "pandas-1.1.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a"}, + {file = "pandas-1.1.5-cp39-cp39-win32.whl", hash = "sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb"}, + {file = "pandas-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782"}, + {file = "pandas-1.1.5.tar.gz", hash = "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"}, +] pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] +pillow = [ + {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"}, + {file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"}, + {file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"}, + {file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"}, + {file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"}, + {file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"}, + {file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"}, + {file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"}, + {file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"}, + {file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"}, + {file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"}, + {file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"}, + {file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"}, + {file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, + {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -1003,6 +1262,10 @@ pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, @@ -1085,6 +1348,31 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +scipy = [ + {file = "scipy-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:821e75f5c16cd7b0ab0ffe7eb9917e5af7b48c25306b4777287de8d792a5f7f3"}, + {file = "scipy-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e7df79b42c3015058a5554bfeab6fd4c9906c46560c9ddebb5c652840f3e182"}, + {file = "scipy-1.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0572256c10ddd058e3d315c555538671ddb2737f27eb56189bfbc3483391403f"}, + {file = "scipy-1.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b77ee5e3a9507622e7f98b16122242a3903397f98d1fe3bc269d904a9025e2bc"}, + {file = "scipy-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:53116abd5060a5b4a58489cf689bee259b779e6b7ecd4ce366e7147aa7c9626e"}, + {file = "scipy-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e7b733d4d98e604109715e11f2ab9340eb45d53f803634ed730039070fc3bc11"}, + {file = "scipy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4ef3d4df8af40cb6f4d4eaf7b02780109ebabeec334cda26a7899ec9d8de9176"}, + {file = "scipy-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd4399d4388ca0239a4825e312b3e61b60f743dd6daf49e5870837716502a92a"}, + {file = "scipy-1.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80df8af7039bce92fb4cd1ceb056258631b11b3c627384e2d29bb48d44c0cae7"}, + {file = "scipy-1.7.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6130e22bf6ee506f7cddde7e0515296d97eb6c6c94f7ef5103c2b77aec5833a7"}, + {file = "scipy-1.7.0-cp38-cp38-win32.whl", hash = "sha256:97ca4552ace1c313707058e774609af59644321e278c3a539322fab2fb09b943"}, + {file = "scipy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5d012cb82cc1dcfa72609abaabb4a4ed8113e3e8ac43464508a418c146be57d"}, + {file = "scipy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5eb8f054eebb351af7490bbb57465ba9662c4e16e1786655c6c7ed530eb9a74e"}, + {file = "scipy-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4b89c223bd09460b52b669e2e642cab73c28855b540e6ed029692546a86f8d"}, + {file = "scipy-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e685fdbfa5b989af4338b29c408b9157ea6addec15d661104c437980c292be5"}, + {file = "scipy-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3595c8b64970c9e5a3f137fa1a9eb64da417e78fb7991d0b098b18a00b776d88"}, + {file = "scipy-1.7.0-cp39-cp39-win32.whl", hash = "sha256:5a983d3cebc27294897951a494cebd78af2eae37facf75d9e4ad4f1f62229860"}, + {file = "scipy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:aef6e922aea6f2e6bbb539b413c85210a9ee32757535b84204ebd22723e69704"}, + {file = "scipy-1.7.0.tar.gz", hash = "sha256:998c5e6ea649489302de2c0bc026ed34284f531df89d2bdc8df3a0d44d165739"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] snowballstemmer = [ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, diff --git a/pyproject.toml b/pyproject.toml index 7c0edfffd..13c80cb78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,10 +5,11 @@ description = "Zama Homomorphic Development frameworK" authors = ["Arthur Meyre "] [tool.poetry.dependencies] -python = ">=3.6.2,<3.10" +python = ">=3.7,<3.10" Sphinx = "^4.1.1" sphinx-rtd-theme = "^0.5.2" myst-parser = "^0.15.1" +networkx = "^2.6.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" From ce358ca838af47ae5af0f315cb26bcc47183dcff Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 16 Jul 2021 17:21:12 +0200 Subject: [PATCH 0019/1104] tests: add test helper to compare digraphs - add test to check that the helper is working --- tests/conftest.py | 29 ++++++++++++++ tests/helpers/test_conftest.py | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/helpers/test_conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..b68f743fe --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +"""PyTest configuration file""" +import networkx as nx +import networkx.algorithms.isomorphism as iso +import pytest + + +class TestHelpers: + """Class allowing to pass helper functions to tests""" + + @staticmethod + def digraphs_are_equivalent(reference: nx.DiGraph, to_compare: nx.DiGraph): + """Check that two digraphs are equivalent without modifications""" + # edge_match is a copy of node_match + edge_matcher = iso.categorical_node_match("input_idx", None) + node_matcher = iso.categorical_node_match("content", None) + graphs_are_isomorphic = nx.is_isomorphic( + reference, + to_compare, + node_match=node_matcher, + edge_match=edge_matcher, + ) + + return graphs_are_isomorphic + + +@pytest.fixture +def test_helpers(): + """Fixture to return the static helper class""" + return TestHelpers diff --git a/tests/helpers/test_conftest.py b/tests/helpers/test_conftest.py new file mode 100644 index 000000000..d1c5b4cc4 --- /dev/null +++ b/tests/helpers/test_conftest.py @@ -0,0 +1,71 @@ +"""Test file for conftest helper functions""" +import networkx as nx + + +def test_digraphs_are_equivalent(test_helpers): + """Function to test digraphs_are_equivalent helper function""" + + class TestNode: + """Dummy test node""" + + computation: str + + def __init__(self, computation: str) -> None: + self.computation = computation + + def __hash__(self) -> int: + return self.computation.__hash__() + + def __eq__(self, other) -> bool: + return self.computation == other.computation + + g_1 = nx.DiGraph() + g_2 = nx.DiGraph() + + t_0 = TestNode("Add") + t_1 = TestNode("Mul") + t_2 = TestNode("TLU") + + g_1.add_edge(t_0, t_2, input_idx=0) + g_1.add_edge(t_1, t_2, input_idx=1) + + # This updates the nodes attributes in the graph + for node in g_1: + g_1.add_node(node, content=node) + + t0p = TestNode("Add") + t1p = TestNode("Mul") + t2p = TestNode("TLU") + + g_2.add_edge(t1p, t2p, input_idx=1) + g_2.add_edge(t0p, t2p, input_idx=0) + + # This updates the nodes attributes in the graph + for node in g_2: + g_2.add_node(node, content=node) + + bad_g2 = nx.DiGraph() + + bad_t0 = TestNode("Not Add") + + bad_g2.add_edge(bad_t0, t_2, input_idx=0) + bad_g2.add_edge(t_1, t_2, input_idx=1) + + # This updates the nodes attributes in the graph + for node in bad_g2: + bad_g2.add_node(node, content=node) + + bad_g3 = nx.DiGraph() + + bad_g3.add_edge(t_0, t_2, input_idx=1) + bad_g3.add_edge(t_1, t_2, input_idx=0) + + # This updates the nodes attributes in the graph + for node in bad_g3: + bad_g3.add_node(node, content=node) + + assert test_helpers.digraphs_are_equivalent(g_1, g_2), "Graphs should be equivalent" + assert not test_helpers.digraphs_are_equivalent(g_1, bad_g2), "Graphs should not be equivalent" + assert not test_helpers.digraphs_are_equivalent(g_2, bad_g2), "Graphs should not be equivalent" + assert not test_helpers.digraphs_are_equivalent(g_1, bad_g3), "Graphs should not be equivalent" + assert not test_helpers.digraphs_are_equivalent(g_2, bad_g3), "Graphs should not be equivalent" From 063b2db9dbc725c2ca72ee3b0dea8ededcb216cb Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 19 Jul 2021 16:16:21 +0200 Subject: [PATCH 0020/1104] feat(data-types): add skeleton of data types, add an Integer type - add convenience functions to instantiate Integer - add tests for the basic functions of integers --- hdk/common/__init__.py | 2 + hdk/common/data_types/__init__.py | 2 + hdk/common/data_types/base.py | 7 +++ hdk/common/data_types/integers.py | 70 ++++++++++++++++++++++++ tests/common/data_types/test_integers.py | 40 ++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 hdk/common/__init__.py create mode 100644 hdk/common/data_types/__init__.py create mode 100644 hdk/common/data_types/base.py create mode 100644 hdk/common/data_types/integers.py create mode 100644 tests/common/data_types/test_integers.py diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py new file mode 100644 index 000000000..ace1dd1c6 --- /dev/null +++ b/hdk/common/__init__.py @@ -0,0 +1,2 @@ +"""HDK's module for shared data structures and code""" +from . import data_types diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py new file mode 100644 index 000000000..256544a3c --- /dev/null +++ b/hdk/common/data_types/__init__.py @@ -0,0 +1,2 @@ +"""HDK's module for data types code and data structures""" +from . import integers diff --git a/hdk/common/data_types/base.py b/hdk/common/data_types/base.py new file mode 100644 index 000000000..47090800d --- /dev/null +++ b/hdk/common/data_types/base.py @@ -0,0 +1,7 @@ +"""File holding code to represent data types in a program""" + +from abc import ABC + + +class BaseDataType(ABC): + """Base class to represent a data type""" diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py new file mode 100644 index 000000000..765b84e8d --- /dev/null +++ b/hdk/common/data_types/integers.py @@ -0,0 +1,70 @@ +"""This file holds the definitions for integer types""" + +from . import base + + +class Integer(base.BaseDataType): + """Class representing an integer""" + + bit_width: int + is_signed: bool + + def __init__(self, bit_width: int, is_signed: bool) -> None: + self.bit_width = bit_width + self.is_signed = is_signed + + def min_value(self) -> int: + """Minimum value representable by the Integer""" + if self.is_signed: + return -(2 ** (self.bit_width - 1)) + + return 0 + + def max_value(self) -> int: + """Maximum value representable by the Integer""" + if self.is_signed: + return 2 ** (self.bit_width - 1) - 1 + + return 2 ** self.bit_width - 1 + + def can_represent_value(self, value_to_represent: int) -> bool: + """A helper function to check if a value is representable by the Integer + + Args: + value_to_represent (int): Value to check + + Returns: + bool: True if the value can be represented by this integer + """ + return self.min_value() <= value_to_represent <= self.max_value() + + +def create_signed_integer(bit_width: int) -> Integer: + """Convenience function to create a signed integer + + Args: + bit_width (int): width of the integer + + Returns: + Integer: A signed integer with the requested bit_width + """ + return Integer(bit_width, is_signed=True) + + +SignedInteger = create_signed_integer + + +def create_unsigned_integer(bit_width: int) -> Integer: + """Convenience function to create an unsigned integer + + Args: + bit_width (int): width of the integer + + Returns: + Integer: An unsigned integer with the requested bit_width + """ + + return Integer(bit_width, is_signed=False) + + +UnsignedInteger = create_unsigned_integer diff --git a/tests/common/data_types/test_integers.py b/tests/common/data_types/test_integers.py new file mode 100644 index 000000000..8e1d12efd --- /dev/null +++ b/tests/common/data_types/test_integers.py @@ -0,0 +1,40 @@ +"""Test file for HDK's common/data_types/integers.py""" + +import random + +import pytest + +from hdk.common.data_types.integers import Integer, SignedInteger, UnsignedInteger + + +@pytest.mark.parametrize( + "integer,expected_min,expected_max", + [ + pytest.param(Integer(8, is_signed=False), 0, 255, id="8 bits unsigned Integer"), + pytest.param(UnsignedInteger(8), 0, 255, id="8 bits UnsignedInteger"), + pytest.param(Integer(8, is_signed=True), -128, 127, id="8 bits signed Integer"), + pytest.param(SignedInteger(8), -128, 127, id="8 bits SignedInteger"), + pytest.param(Integer(32, is_signed=False), 0, 4_294_967_295, id="32 bits unsigned Integer"), + pytest.param(UnsignedInteger(32), 0, 4_294_967_295, id="32 bits UnsignedInteger"), + pytest.param( + Integer(32, is_signed=True), + -2_147_483_648, + 2_147_483_647, + id="32 bits signed Integer", + ), + pytest.param( + SignedInteger(32), + -2_147_483_648, + 2_147_483_647, + id="32 bits SignedInteger", + ), + ], +) +def test_basic_integers(integer: Integer, expected_min: int, expected_max: int): + """Test integer class basic functions""" + assert integer.min_value() == expected_min + assert integer.max_value() == expected_max + + assert integer.can_represent_value(random.randint(expected_min, expected_max)) + assert not integer.can_represent_value(expected_min - 1) + assert not integer.can_represent_value(expected_max + 1) From 1a54bc1f22663537339abf90fe9f6b0000f76e79 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 19 Jul 2021 19:09:15 +0200 Subject: [PATCH 0021/1104] dev(data-types): create Value classes which represent values in a program - value classes have a data_type member to know what they hold - add __repr__ to a few classes to ease readability for debug/print - add helper functions to perform value checks that will be used for tracing to ease readability - add unit tests to get 100% coverage --- hdk/__init__.py | 2 +- hdk/common/data_types/__init__.py | 3 +- hdk/common/data_types/helpers.py | 38 ++++++++++++++++ hdk/common/data_types/integers.py | 4 ++ hdk/common/data_types/values.py | 25 +++++++++++ tests/common/data_types/test_helpers.py | 55 ++++++++++++++++++++++++ tests/common/data_types/test_integers.py | 30 +++++++++++++ tests/common/data_types/test_values.py | 26 +++++++++++ 8 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 hdk/common/data_types/helpers.py create mode 100644 hdk/common/data_types/values.py create mode 100644 tests/common/data_types/test_helpers.py create mode 100644 tests/common/data_types/test_values.py diff --git a/hdk/__init__.py b/hdk/__init__.py index 1df85bfe7..44b5d6169 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -1,2 +1,2 @@ """HDK's top import""" -from . import utils +from . import common, utils diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index 256544a3c..1703a0aaf 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,2 +1,3 @@ """HDK's module for data types code and data structures""" -from . import integers +from . import helpers, integers, values +from .values import BaseValue diff --git a/hdk/common/data_types/helpers.py b/hdk/common/data_types/helpers.py new file mode 100644 index 000000000..892c4be64 --- /dev/null +++ b/hdk/common/data_types/helpers.py @@ -0,0 +1,38 @@ +"""File to hold helper functions for data types related stuff""" + +from typing import cast + +from . import integers, values + +INTEGER_TYPES = set([integers.Integer]) + + +def value_is_encrypted_integer(value_to_check: values.BaseValue) -> bool: + """Helper function to check that a value is an encrypted_integer + + Args: + value_to_check (values.BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is an encrypted value of type Integer + """ + return ( + isinstance(value_to_check, values.EncryptedValue) + and type(value_to_check.data_type) in INTEGER_TYPES + ) + + +def value_is_encrypted_unsigned_integer(value_to_check: values.BaseValue) -> bool: + """Helper function to check that a value is an encrypted_integer + + Args: + value_to_check (values.BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is an encrypted value of type Integer + """ + + return ( + value_is_encrypted_integer(value_to_check) + and not cast(integers.Integer, value_to_check.data_type).is_signed + ) diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index 765b84e8d..d8b431adc 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -13,6 +13,10 @@ class Integer(base.BaseDataType): self.bit_width = bit_width self.is_signed = is_signed + def __repr__(self) -> str: + signed_str = "signed" if self.is_signed else "unsigned" + return f"{self.__class__.__name__}<{signed_str}, {self.bit_width} bits>" + def min_value(self) -> int: """Minimum value representable by the Integer""" if self.is_signed: diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py new file mode 100644 index 000000000..b00ddf5f5 --- /dev/null +++ b/hdk/common/data_types/values.py @@ -0,0 +1,25 @@ +"""File holding classes representing values used by an FHE program""" + +from abc import ABC + +from . import base + + +class BaseValue(ABC): + """Abstract base class to represent any kind of value in a program""" + + data_type: base.BaseDataType + + def __init__(self, data_type: base.BaseDataType) -> None: + self.data_type = data_type + + def __repr__(self) -> str: + return f"{self.__class__.__name__}<{self.data_type!r}>" + + +class ClearValue(BaseValue): + """Class representing a clear/plaintext value (constant or not)""" + + +class EncryptedValue(BaseValue): + """Class representing an encrypted value (constant or not)""" diff --git a/tests/common/data_types/test_helpers.py b/tests/common/data_types/test_helpers.py new file mode 100644 index 000000000..cb76467eb --- /dev/null +++ b/tests/common/data_types/test_helpers.py @@ -0,0 +1,55 @@ +"""Test file for HDK's common/data_types/helpers.py""" + +import pytest + +from hdk.common.data_types.helpers import ( + value_is_encrypted_integer, + value_is_encrypted_unsigned_integer, +) +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearValue, EncryptedValue + + +@pytest.mark.parametrize( + "value,expected_result", + [ + pytest.param( + ClearValue(Integer(8, is_signed=False)), + False, + id="ClearValue 8 bits unsigned Integer", + ), + pytest.param( + EncryptedValue(Integer(8, is_signed=True)), + True, + id="EncryptedValue 8 bits signed Integer", + ), + ], +) +def test_value_is_encrypted_integer(value: Integer, expected_result: bool): + """Test value_is_encrypted_integer helper""" + assert value_is_encrypted_integer(value) == expected_result + + +@pytest.mark.parametrize( + "value,expected_result", + [ + pytest.param( + ClearValue(Integer(8, is_signed=False)), + False, + id="ClearValue 8 bits unsigned Integer", + ), + pytest.param( + EncryptedValue(Integer(8, is_signed=True)), + False, + id="EncryptedValue 8 bits signed Integer", + ), + pytest.param( + EncryptedValue(Integer(8, is_signed=False)), + True, + id="EncryptedValue 8 bits unsigned Integer", + ), + ], +) +def test_value_is_encrypted_unsigned_integer(value: Integer, expected_result: bool): + """Test value_is_encrypted_unsigned_integer helper""" + assert value_is_encrypted_unsigned_integer(value) == expected_result diff --git a/tests/common/data_types/test_integers.py b/tests/common/data_types/test_integers.py index 8e1d12efd..5780cf995 100644 --- a/tests/common/data_types/test_integers.py +++ b/tests/common/data_types/test_integers.py @@ -38,3 +38,33 @@ def test_basic_integers(integer: Integer, expected_min: int, expected_max: int): assert integer.can_represent_value(random.randint(expected_min, expected_max)) assert not integer.can_represent_value(expected_min - 1) assert not integer.can_represent_value(expected_max + 1) + + +@pytest.mark.parametrize( + "integer,expected_repr_str", + [ + pytest.param( + Integer(8, is_signed=False), + "Integer", + id="8 bits unsigned Integer", + ), + pytest.param( + Integer(8, is_signed=True), + "Integer", + id="8 bits signed Integer", + ), + pytest.param( + Integer(32, is_signed=False), + "Integer", + id="32 bits unsigned Integer", + ), + pytest.param( + Integer(32, is_signed=True), + "Integer", + id="32 bits signed Integer", + ), + ], +) +def test_integers_repr(integer: Integer, expected_repr_str: str): + """Test integer repr""" + assert integer.__repr__() == expected_repr_str diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py new file mode 100644 index 000000000..de9803d62 --- /dev/null +++ b/tests/common/data_types/test_values.py @@ -0,0 +1,26 @@ +"""Test file for HDK's common/data_types/values.py""" + +import pytest + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import BaseValue, ClearValue, EncryptedValue + + +@pytest.mark.parametrize( + "value,expected_repr_str", + [ + pytest.param( + ClearValue(Integer(8, is_signed=False)), + "ClearValue>", + id="ClearValue 8 bits unsigned Integer", + ), + pytest.param( + EncryptedValue(Integer(8, is_signed=True)), + "EncryptedValue>", + id="EncryptedValue 8 bits signed Integer", + ), + ], +) +def test_values_repr(value: BaseValue, expected_repr_str: str): + """Test value repr""" + assert value.__repr__() == expected_repr_str From 29c1641f48bcfb90eb0031e593ad2e3f59f19ab6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 21 Jul 2021 10:38:59 +0200 Subject: [PATCH 0022/1104] dev: remove unique_id system, not needed for now - wrong assumption when reading hnp's code, for now unique ids are not needed --- hdk/__init__.py | 2 +- hdk/utils/__init__.py | 3 --- hdk/utils/misc.py | 18 ------------------ tests/utils/test_misc.py | 17 ----------------- 4 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 hdk/utils/__init__.py delete mode 100644 hdk/utils/misc.py delete mode 100644 tests/utils/test_misc.py diff --git a/hdk/__init__.py b/hdk/__init__.py index 44b5d6169..fb269a812 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -1,2 +1,2 @@ """HDK's top import""" -from . import common, utils +from . import common diff --git a/hdk/utils/__init__.py b/hdk/utils/__init__.py deleted file mode 100644 index 314ca624d..000000000 --- a/hdk/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""HDK's utils module""" -from . import misc -from .misc import * diff --git a/hdk/utils/misc.py b/hdk/utils/misc.py deleted file mode 100644 index 197ea760c..000000000 --- a/hdk/utils/misc.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Misc. utils for hdk""" -from typing import Iterator - - -def get_unique_id() -> int: - """Function to get a unique ID""" - - if not hasattr(get_unique_id, "generator"): - - def generator() -> Iterator[int]: - current_id = 0 - while True: - yield current_id - current_id += 1 - - setattr(get_unique_id, "generator", generator()) - - return next(getattr(get_unique_id, "generator")) diff --git a/tests/utils/test_misc.py b/tests/utils/test_misc.py deleted file mode 100644 index e7438adbd..000000000 --- a/tests/utils/test_misc.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Test file for HDK's misc utils""" -import random - -import hdk - - -def test_get_unique_id(): - """Test get_unique_id""" - how_many_ids = random.randint(2, 100) - generated_ids = [hdk.utils.get_unique_id() for __ in range(how_many_ids)] - - len_generated_ids = len(generated_ids) - len_unique_ids = len(set(generated_ids)) - - assert ( - len_generated_ids == len_unique_ids - ), f"Expected to have uniques ids, generated {len_generated_ids}, only had {len_unique_ids}" From b45944b66a034bdb97f688d77958993fc3b9bfb6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 21 Jul 2021 10:41:01 +0200 Subject: [PATCH 0023/1104] dev(ir): add the basis for the frontend IR - add IntermediateNode base class - add Add - add Input --- hdk/common/__init__.py | 2 +- hdk/common/representation/__init__.py | 2 + hdk/common/representation/intermediate.py | 53 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 hdk/common/representation/__init__.py create mode 100644 hdk/common/representation/intermediate.py diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index ace1dd1c6..cd563aaf9 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,2 +1,2 @@ """HDK's module for shared data structures and code""" -from . import data_types +from . import data_types, representation diff --git a/hdk/common/representation/__init__.py b/hdk/common/representation/__init__.py new file mode 100644 index 000000000..7bdfcc4df --- /dev/null +++ b/hdk/common/representation/__init__.py @@ -0,0 +1,2 @@ +"""HDK's representation module to represent source programs""" +from . import intermediate diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py new file mode 100644 index 000000000..7b1cb5433 --- /dev/null +++ b/hdk/common/representation/intermediate.py @@ -0,0 +1,53 @@ +"""File containing HDK's intermdiate representation of source programs operations""" + +from abc import ABC +from copy import deepcopy +from typing import Any, Dict, Iterable, List, Optional, Tuple + +from ..data_types import BaseValue + + +class IntermediateNode(ABC): + """Abstract Base Class to derive from to represent source program operations""" + + inputs: List[BaseValue] + outputs: List[BaseValue] + op_args: Optional[Tuple[Any, ...]] + op_kwargs: Optional[Dict[str, Any]] + + def __init__( + self, + inputs: Iterable[BaseValue], + op_args: Optional[Tuple[Any, ...]] = None, + op_kwargs: Optional[Dict[str, Any]] = None, + ) -> None: + self.inputs = list(inputs) + self.op_args = op_args + self.op_kwargs = op_kwargs + + +class Add(IntermediateNode): + """Addition between two values""" + + def __init__( + self, + inputs: Iterable[BaseValue], + ) -> None: + super().__init__(inputs) + assert len(self.inputs) == 2 + + # For now copy the first input type for the output type + # We don't perform checks or enforce consistency here for now, so this is OK + self.outputs = [deepcopy(self.inputs[0])] + + +class Input(IntermediateNode): + """Node representing an input of the numpy program""" + + def __init__( + self, + inputs: Iterable[BaseValue], + ) -> None: + super().__init__(inputs) + assert len(self.inputs) == 1 + self.outputs = [deepcopy(self.inputs[0])] From a060aaae99f30a25b133f39266d60f9caee9ef8b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 21 Jul 2021 11:09:34 +0200 Subject: [PATCH 0024/1104] feat(tracing): add tracing facilities - add BaseTracer which will hold most of the boilerplate code - add hnumpy with a bare NPTracer and tracing function - update IR to be compatible with tracing helpers - update test helper to properly check that graphs are equivalent - add test tracing a simple addition - rename common/data_types/helpers.py to .../dtypes_helpers.py to avoid having too many files with the same name - ignore missing type stubs in the default mypy command - add a comfort Makefile target to get errors about missing mypy stubs --- Makefile | 9 +- hdk/__init__.py | 2 +- hdk/common/data_types/__init__.py | 2 +- .../{helpers.py => dtypes_helpers.py} | 0 hdk/common/data_types/integers.py | 7 ++ hdk/common/data_types/values.py | 3 + hdk/common/representation/intermediate.py | 40 +++++++- hdk/common/tracing/__init__.py | 7 ++ hdk/common/tracing/base_tracer.py | 67 +++++++++++++ hdk/common/tracing/tracing_helpers.py | 95 +++++++++++++++++++ hdk/hnumpy/__init__.py | 2 + hdk/hnumpy/tracing.py | 48 ++++++++++ ...test_helpers.py => test_dtypes_helpers.py} | 4 +- tests/common/tracing/test_tracing_helpers.py | 26 +++++ tests/conftest.py | 8 +- tests/helpers/test_conftest.py | 12 ++- tests/hnumpy/test_tracing.py | 87 +++++++++++++++++ 17 files changed, 404 insertions(+), 15 deletions(-) rename hdk/common/data_types/{helpers.py => dtypes_helpers.py} (100%) create mode 100644 hdk/common/tracing/__init__.py create mode 100644 hdk/common/tracing/base_tracer.py create mode 100644 hdk/common/tracing/tracing_helpers.py create mode 100644 hdk/hnumpy/__init__.py create mode 100644 hdk/hnumpy/tracing.py rename tests/common/data_types/{test_helpers.py => test_dtypes_helpers.py} (94%) create mode 100644 tests/common/tracing/test_tracing_helpers.py create mode 100644 tests/hnumpy/test_tracing.py diff --git a/Makefile b/Makefile index 02e5a8349..033e1b5d0 100644 --- a/Makefile +++ b/Makefile @@ -30,10 +30,17 @@ pytest: poetry run pytest --cov=hdk -vv --cov-report=xml tests/ .PHONY: pytest +# Not a huge fan of ignoring missing imports, but some packages do not have typing stubs mypy: - poetry run mypy -p hdk + poetry run mypy -p hdk --ignore-missing-imports .PHONY: mypy +# Friendly target to run mypy without ignoring missing stubs and still have errors messages +# Allows to see which stubs we are missing +mypy_ns: + poetry run mypy -p hdk +.PHONY: mypy_ns + docs: cd docs && poetry run make html .PHONY: docs diff --git a/hdk/__init__.py b/hdk/__init__.py index fb269a812..a121a3f0c 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -1,2 +1,2 @@ """HDK's top import""" -from . import common +from . import common, hnumpy diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index 1703a0aaf..5c2244e21 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,3 +1,3 @@ """HDK's module for data types code and data structures""" -from . import helpers, integers, values +from . import dtypes_helpers, integers, values from .values import BaseValue diff --git a/hdk/common/data_types/helpers.py b/hdk/common/data_types/dtypes_helpers.py similarity index 100% rename from hdk/common/data_types/helpers.py rename to hdk/common/data_types/dtypes_helpers.py diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index d8b431adc..91b34d992 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -17,6 +17,13 @@ class Integer(base.BaseDataType): signed_str = "signed" if self.is_signed else "unsigned" return f"{self.__class__.__name__}<{signed_str}, {self.bit_width} bits>" + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and self.bit_width == other.bit_width + and self.is_signed == other.is_signed + ) + def min_value(self) -> int: """Minimum value representable by the Integer""" if self.is_signed: diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index b00ddf5f5..9ca75d249 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -16,6 +16,9 @@ class BaseValue(ABC): def __repr__(self) -> str: return f"{self.__class__.__name__}<{self.data_type!r}>" + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.data_type == other.data_type + class ClearValue(BaseValue): """Class representing a clear/plaintext value (constant or not)""" diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 7b1cb5433..360aa1c05 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -22,9 +22,28 @@ class IntermediateNode(ABC): op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: self.inputs = list(inputs) + assert all(map(lambda x: isinstance(x, BaseValue), self.inputs)) self.op_args = op_args self.op_kwargs = op_kwargs + def is_equivalent_to(self, other: object) -> bool: + """Overriding __eq__ has unwanted side effects, this provides the same facility without + disrupting expected behavior too much + + Args: + other (object): Other object to check against + + Returns: + bool: True if the other object is equivalent + """ + return ( + isinstance(other, self.__class__) + and self.inputs == other.inputs + and self.outputs == other.outputs + and self.op_args == other.op_args + and self.op_kwargs == other.op_kwargs + ) + class Add(IntermediateNode): """Addition between two values""" @@ -32,14 +51,26 @@ class Add(IntermediateNode): def __init__( self, inputs: Iterable[BaseValue], + op_args: Optional[Tuple[Any, ...]] = None, + op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: - super().__init__(inputs) + assert op_args is None, f"Expected op_args to be None, got {op_args}" + assert op_kwargs is None, f"Expected op_kwargs to be None, got {op_kwargs}" + + super().__init__(inputs, op_args=op_args, op_kwargs=op_kwargs) assert len(self.inputs) == 2 # For now copy the first input type for the output type # We don't perform checks or enforce consistency here for now, so this is OK self.outputs = [deepcopy(self.inputs[0])] + def is_equivalent_to(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and (self.inputs == other.inputs or self.inputs == other.inputs[::-1]) + and self.outputs == other.outputs + ) + class Input(IntermediateNode): """Node representing an input of the numpy program""" @@ -47,7 +78,12 @@ class Input(IntermediateNode): def __init__( self, inputs: Iterable[BaseValue], + op_args: Optional[Tuple[Any, ...]] = None, + op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: - super().__init__(inputs) + assert op_args is None, f"Expected op_args to be None, got {op_args}" + assert op_kwargs is None, f"Expected op_kwargs to be None, got {op_kwargs}" + + super().__init__(inputs, op_args=op_args, op_kwargs=op_kwargs) assert len(self.inputs) == 1 self.outputs = [deepcopy(self.inputs[0])] diff --git a/hdk/common/tracing/__init__.py b/hdk/common/tracing/__init__.py new file mode 100644 index 000000000..1818cb5d9 --- /dev/null +++ b/hdk/common/tracing/__init__.py @@ -0,0 +1,7 @@ +"""HDK's module for basic tracing facilities""" +from .base_tracer import BaseTracer +from .tracing_helpers import ( + create_graph_from_output_tracers, + make_input_tracer, + prepare_function_parameters, +) diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py new file mode 100644 index 000000000..925678fb4 --- /dev/null +++ b/hdk/common/tracing/base_tracer.py @@ -0,0 +1,67 @@ +"""This file holds the code that can be shared between tracers""" + +from abc import ABC +from typing import Any, Dict, List, Optional, Tuple, Type + +from ..data_types import BaseValue +from ..representation import intermediate as ir + + +class BaseTracer(ABC): + """Base class for implementing tracers""" + + inputs: List["BaseTracer"] + traced_computation: ir.IntermediateNode + output: BaseValue + + def __init__( + self, + inputs: List["BaseTracer"], + traced_computation: ir.IntermediateNode, + output_index: int, + ) -> None: + self.inputs = inputs + self.traced_computation = traced_computation + self.output = traced_computation.outputs[output_index] + + def instantiate_output_tracers( + self, + inputs: List["BaseTracer"], + computation_to_trace: Type[ir.IntermediateNode], + op_args: Optional[Tuple[Any, ...]] = None, + op_kwargs: Optional[Dict[str, Any]] = None, + ) -> Tuple["BaseTracer", ...]: + """Helper functions to instantiate all output BaseTracer for a given computation + + Args: + inputs (List[BaseTracer]): Previous BaseTracer used as inputs for a new node + computation_to_trace (Type[ir.IntermediateNode]): The IntermediateNode class + to instantiate for the computation being traced + op_args: *args coming from the call being traced + op_kwargs: **kwargs coming from the call being traced + + + Returns: + Tuple[BaseTracer, ...]: A tuple containing an BaseTracer per output function + """ + traced_computation = computation_to_trace( + map(lambda x: x.output, inputs), + op_args=op_args, + op_kwargs=op_kwargs, + ) + + output_tracers = tuple( + self.__class__(inputs, traced_computation, output_index) + for output_index in range(len(traced_computation.outputs)) + ) + + return output_tracers + + def __add__(self, other: "BaseTracer") -> "BaseTracer": + result_tracer = self.instantiate_output_tracers( + [self, other], + ir.Add, + ) + + assert len(result_tracer) == 1 + return result_tracer[0] diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py new file mode 100644 index 000000000..a101ec5db --- /dev/null +++ b/hdk/common/tracing/tracing_helpers.py @@ -0,0 +1,95 @@ +"""Helper functions for tracing""" +from inspect import signature +from typing import Callable, Dict, Iterable, Set, Tuple, Type + +import networkx as nx +from networkx.algorithms.dag import is_directed_acyclic_graph + +from ..data_types import BaseValue +from ..representation import intermediate as ir +from .base_tracer import BaseTracer + + +def make_input_tracer(tracer_class: Type[BaseTracer], input_value: BaseValue) -> BaseTracer: + """Helper function to create a tracer for an input value + + Args: + tracer_class (Type[BaseTracer]): the class of tracer to create an Input for + input_value (BaseValue): the Value that is an input and needs to be wrapped in an + BaseTracer + + Returns: + BaseTracer: The BaseTracer for that input value + """ + return tracer_class([], ir.Input([input_value]), 0) + + +def prepare_function_parameters( + function_to_trace: Callable, function_parameters: Dict[str, BaseValue] +) -> Dict[str, BaseValue]: + """Function to filter the passed function_parameters to trace function_to_trace + + Args: + function_to_trace (Callable): function that will be traced for which parameters are checked + function_parameters (Dict[str, BaseValue]): parameters given to trace the function + + Raises: + ValueError: Raised when some parameters are missing to trace function_to_trace + + Returns: + Dict[str, BaseValue]: filtered function_parameters dictionary + """ + function_signature = signature(function_to_trace) + + missing_args = function_signature.parameters.keys() - function_parameters.keys() + + if len(missing_args) > 0: + raise ValueError( + f"The function '{function_to_trace.__name__}' requires the following parameters" + f"that were not provided: {', '.join(sorted(missing_args))}" + ) + + useless_arguments = function_parameters.keys() - function_signature.parameters.keys() + useful_arguments = function_signature.parameters.keys() - useless_arguments + + return {k: function_parameters[k] for k in useful_arguments} + + +def create_graph_from_output_tracers( + output_tracers: Iterable[BaseTracer], +) -> nx.MultiDiGraph: + """Generate a networkx Directed Graph that will represent the computation from a traced function + + Args: + output_tracers (Iterable[BaseTracer]): the output tracers resulting from running the + function over the proper input tracers + + Returns: + nx.MultiDiGraph: Directed Graph that is guaranteed to be a DAG containing the ir nodes + representing the traced program/function + """ + graph = nx.MultiDiGraph() + + visited_tracers: Set[BaseTracer] = set() + current_tracers = tuple(output_tracers) + + while current_tracers: + next_tracers: Tuple[BaseTracer, ...] = tuple() + for tracer in current_tracers: + current_ir_node = tracer.traced_computation + graph.add_node(current_ir_node, content=current_ir_node) + + for input_idx, input_tracer in enumerate(tracer.inputs): + input_ir_node = input_tracer.traced_computation + graph.add_node(input_ir_node, content=input_ir_node) + graph.add_edge(input_ir_node, current_ir_node, input_idx=input_idx) + if input_tracer not in visited_tracers: + next_tracers += (input_tracer,) + + visited_tracers.add(tracer) + + current_tracers = next_tracers + + assert is_directed_acyclic_graph(graph) + + return graph diff --git a/hdk/hnumpy/__init__.py b/hdk/hnumpy/__init__.py new file mode 100644 index 000000000..5af83dc23 --- /dev/null +++ b/hdk/hnumpy/__init__.py @@ -0,0 +1,2 @@ +"""HDK's module for compiling numpy functions to homomorphic equivalents""" +from . import tracing diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py new file mode 100644 index 000000000..8e1ac38a9 --- /dev/null +++ b/hdk/hnumpy/tracing.py @@ -0,0 +1,48 @@ +"""hnumpy tracing utilities""" +from typing import Callable, Dict + +import networkx as nx + +from ..common.data_types import BaseValue +from ..common.tracing import ( + BaseTracer, + create_graph_from_output_tracers, + make_input_tracer, + prepare_function_parameters, +) + + +class NPTracer(BaseTracer): + """Tracer class for numpy operations""" + + +def trace_numpy_function( + function_to_trace: Callable, function_parameters: Dict[str, BaseValue] +) -> nx.MultiDiGraph: + """Function used to trace a numpy function + + Args: + function_to_trace (Callable): The function you want to trace + function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the + function is e.g. an EncryptedValue holding a 7bits unsigned Integer + + Returns: + nx.MultiDiGraph: The graph containing the ir nodes representing the computation done in the + input function + """ + function_parameters = prepare_function_parameters(function_to_trace, function_parameters) + + input_tracers = { + param_name: make_input_tracer(NPTracer, param) + for param_name, param in function_parameters.items() + } + + # We could easily create a graph of NPTracer, but we may end up with dead nodes starting from + # the inputs that's why we create the graph starting from the outputs + output_tracers = function_to_trace(**input_tracers) + if isinstance(output_tracers, NPTracer): + output_tracers = (output_tracers,) + + graph = create_graph_from_output_tracers(output_tracers) + + return graph diff --git a/tests/common/data_types/test_helpers.py b/tests/common/data_types/test_dtypes_helpers.py similarity index 94% rename from tests/common/data_types/test_helpers.py rename to tests/common/data_types/test_dtypes_helpers.py index cb76467eb..43b5fd872 100644 --- a/tests/common/data_types/test_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -1,8 +1,8 @@ -"""Test file for HDK's common/data_types/helpers.py""" +"""Test file for HDK's data types helpers""" import pytest -from hdk.common.data_types.helpers import ( +from hdk.common.data_types.dtypes_helpers import ( value_is_encrypted_integer, value_is_encrypted_unsigned_integer, ) diff --git a/tests/common/tracing/test_tracing_helpers.py b/tests/common/tracing/test_tracing_helpers.py new file mode 100644 index 000000000..20adb02ad --- /dev/null +++ b/tests/common/tracing/test_tracing_helpers.py @@ -0,0 +1,26 @@ +"""Test file for HDK's common tracing helpers""" + +from typing import Any, Dict + +import pytest + +from hdk.common.tracing.tracing_helpers import prepare_function_parameters + + +@pytest.mark.parametrize( + "function,function_parameters,ref_dict", + [ + pytest.param(lambda x: None, {}, {}, id="Missing x", marks=pytest.mark.xfail(strict=True)), + pytest.param(lambda x: None, {"x": None}, {"x": None}, id="Only x"), + pytest.param( + lambda x: None, {"x": None, "y": None}, {"x": None}, id="Additional y filtered" + ), + ], +) +def test_prepare_function_parameters( + function, function_parameters: Dict[str, Any], ref_dict: Dict[str, Any] +): + """Test prepare_function_parameters""" + prepared_dict = prepare_function_parameters(function, function_parameters) + + assert prepared_dict == ref_dict diff --git a/tests/conftest.py b/tests/conftest.py index b68f743fe..93206f8f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,11 +8,13 @@ class TestHelpers: """Class allowing to pass helper functions to tests""" @staticmethod - def digraphs_are_equivalent(reference: nx.DiGraph, to_compare: nx.DiGraph): + def digraphs_are_equivalent(reference: nx.MultiDiGraph, to_compare: nx.MultiDiGraph): """Check that two digraphs are equivalent without modifications""" # edge_match is a copy of node_match - edge_matcher = iso.categorical_node_match("input_idx", None) - node_matcher = iso.categorical_node_match("content", None) + edge_matcher = iso.categorical_multiedge_match("input_idx", None) + node_matcher = iso.generic_node_match( + "content", None, lambda lhs, rhs: lhs.is_equivalent_to(rhs) + ) graphs_are_isomorphic = nx.is_isomorphic( reference, to_compare, diff --git a/tests/helpers/test_conftest.py b/tests/helpers/test_conftest.py index d1c5b4cc4..9ed6185af 100644 --- a/tests/helpers/test_conftest.py +++ b/tests/helpers/test_conftest.py @@ -16,11 +16,13 @@ def test_digraphs_are_equivalent(test_helpers): def __hash__(self) -> int: return self.computation.__hash__() - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: return self.computation == other.computation - g_1 = nx.DiGraph() - g_2 = nx.DiGraph() + is_equivalent_to = __eq__ + + g_1 = nx.MultiDiGraph() + g_2 = nx.MultiDiGraph() t_0 = TestNode("Add") t_1 = TestNode("Mul") @@ -44,7 +46,7 @@ def test_digraphs_are_equivalent(test_helpers): for node in g_2: g_2.add_node(node, content=node) - bad_g2 = nx.DiGraph() + bad_g2 = nx.MultiDiGraph() bad_t0 = TestNode("Not Add") @@ -55,7 +57,7 @@ def test_digraphs_are_equivalent(test_helpers): for node in bad_g2: bad_g2.add_node(node, content=node) - bad_g3 = nx.DiGraph() + bad_g3 = nx.MultiDiGraph() bad_g3.add_edge(t_0, t_2, input_idx=1) bad_g3.add_edge(t_1, t_2, input_idx=0) diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py new file mode 100644 index 000000000..1dbe878ec --- /dev/null +++ b/tests/hnumpy/test_tracing.py @@ -0,0 +1,87 @@ +"""Test file for HDK's hnumpy tracing""" + +import networkx as nx +import pytest + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearValue, EncryptedValue +from hdk.common.representation import intermediate as ir +from hdk.hnumpy import tracing + + +@pytest.mark.parametrize( + "x", + [ + pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), + pytest.param( + EncryptedValue(Integer(64, is_signed=True)), + id="Encrypted int", + ), + pytest.param( + ClearValue(Integer(64, is_signed=False)), + id="Clear uint", + ), + pytest.param( + ClearValue(Integer(64, is_signed=True)), + id="Clear int", + ), + ], +) +@pytest.mark.parametrize( + "y", + [ + pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), + pytest.param( + EncryptedValue(Integer(64, is_signed=True)), + id="Encrypted int", + ), + pytest.param( + ClearValue(Integer(64, is_signed=False)), + id="Clear uint", + ), + pytest.param( + ClearValue(Integer(64, is_signed=True)), + id="Clear int", + ), + ], +) +def test_hnumpy_tracing_add(x, y, test_helpers): + "Test hnumpy tracing __add__" + + def simple_add_function(x, y): + z = x + x + return z + y + + graph = tracing.trace_numpy_function(simple_add_function, {"x": x, "y": y}) + + ref_graph = nx.MultiDiGraph() + + input_x = ir.Input((x,)) + input_y = ir.Input((y,)) + + add_node_z = ir.Add( + ( + input_x.outputs[0], + input_x.outputs[0], + ) + ) + + return_add_node = ir.Add( + ( + add_node_z.outputs[0], + input_y.outputs[0], + ) + ) + + ref_graph.add_node(input_x, content=input_x) + ref_graph.add_node(input_y, content=input_y) + ref_graph.add_node(add_node_z, content=add_node_z) + ref_graph.add_node(return_add_node, content=return_add_node) + + ref_graph.add_edge(input_x, add_node_z, input_idx=0) + ref_graph.add_edge(input_x, add_node_z, input_idx=1) + + ref_graph.add_edge(add_node_z, return_add_node, input_idx=0) + ref_graph.add_edge(input_y, return_add_node, input_idx=1) + + assert test_helpers.digraphs_are_equivalent(ref_graph, graph) From b8839959113f0f4e26f12a3385566eeb0762b086 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 23 Jul 2021 10:36:45 +0200 Subject: [PATCH 0025/1104] docs(dev): update frontend flow file - use feedbacks and re-evaluate with experience building the numpy tracing --- docs/_static/frontend_flow.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/frontend_flow.svg b/docs/_static/frontend_flow.svg index 4d7788693..28235ea17 100644 --- a/docs/_static/frontend_flow.svg +++ b/docs/_static/frontend_flow.svg @@ -1,3 +1,3 @@ -
Input Program
v0: numpy
Input Program...
Tracing
Tracing
Data
Data
Algorithm/Function/Transform
Algorithm/Function/Transform
Operator DAG:
"Base Graph"
Operator DAG:...
Topological transform
Topological transform
Operator DAG:
"Candidate Graph"
Operator DAG:...
Conformance check
Conformance check
Constraints
Constraints
Input/Intermediate/Output values: 7b unsigned int
Constants: 7+1 = 8b signless int
Input/Intermediate/Output values: 7b unsigned int...
UI/UX
UI/UX
Error Message + Debug Infos
Error Message + Debug Infos
NO
NO
Bounds Measurement
Bounds Measurement
Dataset + Evaluation
Dataset + Evaluation
Input Bounds + Propagation
Input Bounds + Propagation
YES
YES
OR
OR
Data Widths
Data Widths
Opset v0: ADD, MUL, DOT, TLU
Opset v0: ADD, MUL, DOT, TLU
TLU Instantiation
TLU Instantiation
Operator DAG + Width:
"Compilable Graph"
Operator DAG + Width:...
MLIR Lowering
MLIR Lowering
MLIR
MLIR
Compiler "Backend"
Compiler "Backend"
Viewer does not support full SVG 1.1
\ No newline at end of file +
Input Program
v0: numpy
Input Program...
Tracing
Tracing
Data
Data
Algorithm/Function/Transform
Algorithm/Function/Transform
Operator DAG:
"Base Graph"
Operator DAG:...
Topological transform
Topological transform
Operator DAG:
"Candidate Graph"
Operator DAG:...
Constraints check
Constraints check
Input/Intermediate/Output values: 7b unsigned int
Constants: 7+1 = 8b signless int

AND

Opset v0: ADD, MUL, DOT, TLU
Input/Intermediate/Output values: 7b unsigned int...
UI/UX
UI/UX
Error Message + Debug Infos
Error Message + Debug Infos
Bounds Measurement
Bounds Measurement
Dataset + Evaluation
Dataset + Evaluation
Input Bounds + Propagation
Input Bounds + Propagation
OR
OR
Operator DAG + Width:
"Compilable Graph"
Operator DAG + Width:...
MLIR Lowering
MLIR Lowering
MLIR
MLIR
Compiler "Backend"
Compiler "Backend"
Input/Intermediate/Output values: unsigned int
Constants: signless int
Input/Intermediate/Output values: unsigned int...
Error Message + Debug Infos
Error Message + Debug Infos
Viewer does not support full SVG 1.1
\ No newline at end of file From deb7631a3a9e6cf611e4c1ae624e08f5ada355d3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 26 Jul 2021 18:00:39 +0200 Subject: [PATCH 0026/1104] chore(tools): add mypy_test and mypy_ci target - update tests that were failing with new mypy check - mypy_test runs mypy on all .py source files in tests - mypy_ci runs mypy and mypy_test, mypy is for source i.e. hdk/ only --- Makefile | 9 ++++++++- tests/common/data_types/test_dtypes_helpers.py | 6 +++--- tests/helpers/test_conftest.py | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 033e1b5d0..041b36c33 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ pylint: conformance: python_format .PHONY: conformance -pcc: check_python_format pylint mypy +pcc: check_python_format pylint mypy_ci .PHONY: pcc pytest: @@ -41,6 +41,13 @@ mypy_ns: poetry run mypy -p hdk .PHONY: mypy_ns +mypy_test: + find ./tests/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports +.PHONY: mypy_test + +mypy_ci: mypy mypy_test +.PHONY: mypy_ci + docs: cd docs && poetry run make html .PHONY: docs diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 43b5fd872..683f04dd2 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -7,7 +7,7 @@ from hdk.common.data_types.dtypes_helpers import ( value_is_encrypted_unsigned_integer, ) from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearValue, EncryptedValue +from hdk.common.data_types.values import BaseValue, ClearValue, EncryptedValue @pytest.mark.parametrize( @@ -25,7 +25,7 @@ from hdk.common.data_types.values import ClearValue, EncryptedValue ), ], ) -def test_value_is_encrypted_integer(value: Integer, expected_result: bool): +def test_value_is_encrypted_integer(value: BaseValue, expected_result: bool): """Test value_is_encrypted_integer helper""" assert value_is_encrypted_integer(value) == expected_result @@ -50,6 +50,6 @@ def test_value_is_encrypted_integer(value: Integer, expected_result: bool): ), ], ) -def test_value_is_encrypted_unsigned_integer(value: Integer, expected_result: bool): +def test_value_is_encrypted_unsigned_integer(value: BaseValue, expected_result: bool): """Test value_is_encrypted_unsigned_integer helper""" assert value_is_encrypted_unsigned_integer(value) == expected_result diff --git a/tests/helpers/test_conftest.py b/tests/helpers/test_conftest.py index 9ed6185af..50b8f7d53 100644 --- a/tests/helpers/test_conftest.py +++ b/tests/helpers/test_conftest.py @@ -17,7 +17,7 @@ def test_digraphs_are_equivalent(test_helpers): return self.computation.__hash__() def __eq__(self, other: object) -> bool: - return self.computation == other.computation + return isinstance(other, self.__class__) and self.computation == other.computation is_equivalent_to = __eq__ From f2140423b7a63c99d498c7251241be1168244130 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 26 Jul 2021 17:29:59 +0200 Subject: [PATCH 0027/1104] feat: adding management of Sub and Mul tests: add the possibility to test more binary operations avoid copy-paste for binary ops, with just an option if the operation is commutative or not refs #41, #42 --- hdk/common/representation/intermediate.py | 63 ++++++++++++++++------- hdk/common/tracing/base_tracer.py | 18 +++++++ tests/hnumpy/test_tracing.py | 38 +++++++++++--- 3 files changed, 93 insertions(+), 26 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 360aa1c05..1c74ec031 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -26,6 +26,37 @@ class IntermediateNode(ABC): self.op_args = op_args self.op_kwargs = op_kwargs + def _init_binary( + self, + inputs: Iterable[BaseValue], + op_args: Optional[Tuple[Any, ...]] = None, + op_kwargs: Optional[Dict[str, Any]] = None, + ) -> None: + assert op_args is None, f"Expected op_args to be None, got {op_args}" + assert op_kwargs is None, f"Expected op_kwargs to be None, got {op_kwargs}" + + IntermediateNode.__init__(self, inputs, op_args=op_args, op_kwargs=op_kwargs) + + assert len(self.inputs) == 2 + + # For now copy the first input type for the output type + # We don't perform checks or enforce consistency here for now, so this is OK + self.outputs = [deepcopy(self.inputs[0])] + + def _is_equivalent_to_binary_commutative(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and (self.inputs == other.inputs or self.inputs == other.inputs[::-1]) + and self.outputs == other.outputs + ) + + def _is_equivalent_to_binary_non_commutative(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and self.inputs == other.inputs + and self.outputs == other.outputs + ) + def is_equivalent_to(self, other: object) -> bool: """Overriding __eq__ has unwanted side effects, this provides the same facility without disrupting expected behavior too much @@ -48,28 +79,22 @@ class IntermediateNode(ABC): class Add(IntermediateNode): """Addition between two values""" - def __init__( - self, - inputs: Iterable[BaseValue], - op_args: Optional[Tuple[Any, ...]] = None, - op_kwargs: Optional[Dict[str, Any]] = None, - ) -> None: - assert op_args is None, f"Expected op_args to be None, got {op_args}" - assert op_kwargs is None, f"Expected op_kwargs to be None, got {op_kwargs}" + __init__ = IntermediateNode._init_binary + is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative - super().__init__(inputs, op_args=op_args, op_kwargs=op_kwargs) - assert len(self.inputs) == 2 - # For now copy the first input type for the output type - # We don't perform checks or enforce consistency here for now, so this is OK - self.outputs = [deepcopy(self.inputs[0])] +class Sub(IntermediateNode): + """Subtraction between two values""" - def is_equivalent_to(self, other: object) -> bool: - return ( - isinstance(other, self.__class__) - and (self.inputs == other.inputs or self.inputs == other.inputs[::-1]) - and self.outputs == other.outputs - ) + __init__ = IntermediateNode._init_binary + is_equivalent_to = IntermediateNode._is_equivalent_to_binary_non_commutative + + +class Mul(IntermediateNode): + """Multiplication between two values""" + + __init__ = IntermediateNode._init_binary + is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative class Input(IntermediateNode): diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 925678fb4..73b674fef 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -65,3 +65,21 @@ class BaseTracer(ABC): assert len(result_tracer) == 1 return result_tracer[0] + + def __sub__(self, other: "BaseTracer") -> "BaseTracer": + result_tracer = self.instantiate_output_tracers( + [self, other], + ir.Sub, + ) + + assert len(result_tracer) == 1 + return result_tracer[0] + + def __mul__(self, other: "BaseTracer") -> "BaseTracer": + result_tracer = self.instantiate_output_tracers( + [self, other], + ir.Mul, + ) + + assert len(result_tracer) == 1 + return result_tracer[0] diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 1dbe878ec..7d4e8b132 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -9,6 +9,10 @@ from hdk.common.representation import intermediate as ir from hdk.hnumpy import tracing +@pytest.mark.parametrize( + "operation", + [ir.Add, ir.Sub, ir.Mul], +) @pytest.mark.parametrize( "x", [ @@ -45,14 +49,34 @@ from hdk.hnumpy import tracing ), ], ) -def test_hnumpy_tracing_add(x, y, test_helpers): - "Test hnumpy tracing __add__" +def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): + "Test hnumpy tracing a binary operation (in the supported ops)" + # Remark that the functions here have a common structure (which is + # 2x op y), such that creating further the ref_graph is easy, by + # hand def simple_add_function(x, y): z = x + x return z + y - graph = tracing.trace_numpy_function(simple_add_function, {"x": x, "y": y}) + def simple_sub_function(x, y): + z = x + x + return z - y + + def simple_mul_function(x, y): + z = x + x + return z * y + + if operation == ir.Add: + function_to_compile = simple_add_function + elif operation == ir.Sub: + function_to_compile = simple_sub_function + elif operation == ir.Mul: + function_to_compile = simple_mul_function + else: + assert False, f"unknown operation {operation}" + + graph = tracing.trace_numpy_function(function_to_compile, {"x": x, "y": y}) ref_graph = nx.MultiDiGraph() @@ -66,7 +90,7 @@ def test_hnumpy_tracing_add(x, y, test_helpers): ) ) - return_add_node = ir.Add( + returned_final_node = operation( ( add_node_z.outputs[0], input_y.outputs[0], @@ -76,12 +100,12 @@ def test_hnumpy_tracing_add(x, y, test_helpers): ref_graph.add_node(input_x, content=input_x) ref_graph.add_node(input_y, content=input_y) ref_graph.add_node(add_node_z, content=add_node_z) - ref_graph.add_node(return_add_node, content=return_add_node) + ref_graph.add_node(returned_final_node, content=returned_final_node) ref_graph.add_edge(input_x, add_node_z, input_idx=0) ref_graph.add_edge(input_x, add_node_z, input_idx=1) - ref_graph.add_edge(add_node_z, return_add_node, input_idx=0) - ref_graph.add_edge(input_y, return_add_node, input_idx=1) + ref_graph.add_edge(add_node_z, returned_final_node, input_idx=0) + ref_graph.add_edge(input_y, returned_final_node, input_idx=1) assert test_helpers.digraphs_are_equivalent(ref_graph, graph) From 95f05800d5169768b6ccb6ce03a1e19de1182e4f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 27 Jul 2021 10:45:13 +0200 Subject: [PATCH 0028/1104] chore(tools): allow to have fixmes in the code without failing CI --- pylintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/pylintrc b/pylintrc index 07f7a7a29..388215226 100644 --- a/pylintrc +++ b/pylintrc @@ -92,6 +92,7 @@ disable=print-statement, useless-suppression, deprecated-pragma, use-symbolic-message-instead, + fixme, apply-builtin, basestring-builtin, buffer-builtin, From f910f1fa9cc11cc09558b3495389a89abc23f8af Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 26 Jul 2021 14:23:35 +0200 Subject: [PATCH 0029/1104] chore(tools): add user friendly coverage make command - add a command allowing to run tests and coverage in one go --- Makefile | 7 +++++++ script/actions_utils/coverage.sh | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 041b36c33..855351dc8 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,13 @@ mypy_test: mypy_ci: mypy mypy_test .PHONY: mypy_ci +pytest_and_coverage: pytest coverage +.PHONY: pytest_and_coverage + +coverage: + @if [[ "$$BB" == "" ]]; then BB=origin/main; fi && poetry run diff-cover coverage.xml --fail-under 100 --html-report coverage.html --compare-branch $$BB +.PHONY: coverage + docs: cd docs && poetry run make html .PHONY: docs diff --git a/script/actions_utils/coverage.sh b/script/actions_utils/coverage.sh index 6389a4e64..9fbd08376 100755 --- a/script/actions_utils/coverage.sh +++ b/script/actions_utils/coverage.sh @@ -6,9 +6,7 @@ set +e CURR_DIR=`dirname $0` # Run diff-coverage -poetry run diff-cover coverage.xml --fail-under 100 \ ---html-report coverage.html \ ---compare-branch origin/"$1" | tee diff-coverage.txt +BB="origin/$1" make coverage | tee diff-coverage.txt # Get exit code without closing the script TEST_EXIT_CODE="$?" From a56a0dbf0c7f9aabd9d89d89235e68978fcf3700 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 23 Jul 2021 16:05:31 +0200 Subject: [PATCH 0030/1104] dev(ir): make Input ir node accept a name --- hdk/common/representation/intermediate.py | 13 ++++++------- hdk/common/tracing/tracing_helpers.py | 9 +++++++-- hdk/hnumpy/tracing.py | 2 +- tests/hnumpy/test_tracing.py | 4 ++-- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 1c74ec031..bf9da6dde 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -100,15 +100,14 @@ class Mul(IntermediateNode): class Input(IntermediateNode): """Node representing an input of the numpy program""" + input_name: str + def __init__( self, - inputs: Iterable[BaseValue], - op_args: Optional[Tuple[Any, ...]] = None, - op_kwargs: Optional[Dict[str, Any]] = None, + input_value: BaseValue, + input_name: str, ) -> None: - assert op_args is None, f"Expected op_args to be None, got {op_args}" - assert op_kwargs is None, f"Expected op_kwargs to be None, got {op_kwargs}" - - super().__init__(inputs, op_args=op_args, op_kwargs=op_kwargs) + super().__init__((input_value,)) assert len(self.inputs) == 1 + self.input_name = input_name self.outputs = [deepcopy(self.inputs[0])] diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py index a101ec5db..e7b1dba42 100644 --- a/hdk/common/tracing/tracing_helpers.py +++ b/hdk/common/tracing/tracing_helpers.py @@ -10,18 +10,23 @@ from ..representation import intermediate as ir from .base_tracer import BaseTracer -def make_input_tracer(tracer_class: Type[BaseTracer], input_value: BaseValue) -> BaseTracer: +def make_input_tracer( + tracer_class: Type[BaseTracer], + input_name: str, + input_value: BaseValue, +) -> BaseTracer: """Helper function to create a tracer for an input value Args: tracer_class (Type[BaseTracer]): the class of tracer to create an Input for + input_name (str): the name of the input in the traced function input_value (BaseValue): the Value that is an input and needs to be wrapped in an BaseTracer Returns: BaseTracer: The BaseTracer for that input value """ - return tracer_class([], ir.Input([input_value]), 0) + return tracer_class([], ir.Input(input_value, input_name), 0) def prepare_function_parameters( diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 8e1ac38a9..c55d97124 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -33,7 +33,7 @@ def trace_numpy_function( function_parameters = prepare_function_parameters(function_to_trace, function_parameters) input_tracers = { - param_name: make_input_tracer(NPTracer, param) + param_name: make_input_tracer(NPTracer, param_name, param) for param_name, param in function_parameters.items() } diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 7d4e8b132..07b4acbc1 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -80,8 +80,8 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): ref_graph = nx.MultiDiGraph() - input_x = ir.Input((x,)) - input_y = ir.Input((y,)) + input_x = ir.Input(x, input_name="x") + input_y = ir.Input(y, input_name="y") add_node_z = ir.Add( ( From d7c1f4236395b946bfd14ca082c70ed825c485ab Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 26 Jul 2021 14:31:49 +0200 Subject: [PATCH 0031/1104] dev(dtypes): add functions to mix values and find dtype to hold two inputs - allows to have a generic way of propagating types instead of deepcopying input values and types in IR nodes - supports only Integers for now - opset checks will not be performed in those functions to keep knowledge required on the opset to the MLIR conversion step or extra check steps - use the mix_values_determine_holding_dtype in intermediate nodes where appropriate --- hdk/common/data_types/dtypes_helpers.py | 94 +++++++++++++++++-- hdk/common/representation/intermediate.py | 5 +- .../common/data_types/test_dtypes_helpers.py | 85 +++++++++++++++++ 3 files changed, 173 insertions(+), 11 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 892c4be64..85f6ad2f7 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -2,31 +2,33 @@ from typing import cast -from . import integers, values +from .base import BaseDataType +from .integers import Integer +from .values import BaseValue, ClearValue, EncryptedValue -INTEGER_TYPES = set([integers.Integer]) +INTEGER_TYPES = set([Integer]) -def value_is_encrypted_integer(value_to_check: values.BaseValue) -> bool: +def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: """Helper function to check that a value is an encrypted_integer Args: - value_to_check (values.BaseValue): The value to check + value_to_check (BaseValue): The value to check Returns: bool: True if the passed value_to_check is an encrypted value of type Integer """ return ( - isinstance(value_to_check, values.EncryptedValue) + isinstance(value_to_check, EncryptedValue) and type(value_to_check.data_type) in INTEGER_TYPES ) -def value_is_encrypted_unsigned_integer(value_to_check: values.BaseValue) -> bool: +def value_is_encrypted_unsigned_integer(value_to_check: BaseValue) -> bool: """Helper function to check that a value is an encrypted_integer Args: - value_to_check (values.BaseValue): The value to check + value_to_check (BaseValue): The value to check Returns: bool: True if the passed value_to_check is an encrypted value of type Integer @@ -34,5 +36,81 @@ def value_is_encrypted_unsigned_integer(value_to_check: values.BaseValue) -> boo return ( value_is_encrypted_integer(value_to_check) - and not cast(integers.Integer, value_to_check.data_type).is_signed + and not cast(Integer, value_to_check.data_type).is_signed ) + + +def find_type_to_hold_both_lossy( + dtype1: BaseDataType, + dtype2: BaseDataType, +) -> BaseDataType: + """Determine the type that can represent both dtype1 and dtype2 separately, this is lossy with + floating point types + + Args: + dtype1 (BaseDataType): first dtype to hold + dtype2 (BaseDataType): second dtype to hold + + Raises: + NotImplementedError: Raised if one of the two input dtypes is not an Integer as they are the + only type supported for now + + Returns: + BaseDataType: The dtype able to hold (potentially lossy) dtype1 and dtype2 + """ + if isinstance(dtype1, Integer) and isinstance(dtype2, Integer): + d1_signed = dtype1.is_signed + d2_signed = dtype2.is_signed + max_bits = max(dtype1.bit_width, dtype2.bit_width) + + holding_integer: BaseDataType + + if d1_signed and d2_signed: + holding_integer = Integer(max_bits, is_signed=True) + elif not d1_signed and not d2_signed: + holding_integer = Integer(max_bits, is_signed=False) + elif d1_signed and not d2_signed: + # 2 is unsigned, if it has the bigger bit_width, we need a signed integer that can hold + # it, so add 1 bit of sign to its bit_width + if dtype2.bit_width >= dtype1.bit_width: + new_bit_width = dtype2.bit_width + 1 + holding_integer = Integer(new_bit_width, is_signed=True) + else: + holding_integer = Integer(dtype1.bit_width, is_signed=True) + elif not d1_signed and d2_signed: + # Same as above, with 1 and 2 switched around + if dtype1.bit_width >= dtype2.bit_width: + new_bit_width = dtype1.bit_width + 1 + holding_integer = Integer(new_bit_width, is_signed=True) + else: + holding_integer = Integer(dtype2.bit_width, is_signed=True) + + return holding_integer + + raise NotImplementedError("For now only Integers are supported by find_type_to_hold_both_lossy") + + +def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> BaseValue: + """Returns a Value that would result from computation on both value1 and value2 while + determining the data type able to hold both value1 and value2 data type (this can be lossy + with floats) + + Args: + value1 (BaseValue): first value to mix + value2 (BaseValue): second value to mix + + Returns: + BaseValue: The resulting mixed value with data type able to hold both value1 and value2 + dtypes + """ + + holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) + + mixed_value: BaseValue + + if isinstance(value1, EncryptedValue) or isinstance(value2, EncryptedValue): + mixed_value = EncryptedValue(holding_type) + else: + mixed_value = ClearValue(holding_type) + + return mixed_value diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index bf9da6dde..d18612cfa 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -5,6 +5,7 @@ from copy import deepcopy from typing import Any, Dict, Iterable, List, Optional, Tuple from ..data_types import BaseValue +from ..data_types.dtypes_helpers import mix_values_determine_holding_dtype class IntermediateNode(ABC): @@ -39,9 +40,7 @@ class IntermediateNode(ABC): assert len(self.inputs) == 2 - # For now copy the first input type for the output type - # We don't perform checks or enforce consistency here for now, so this is OK - self.outputs = [deepcopy(self.inputs[0])] + self.outputs = [mix_values_determine_holding_dtype(self.inputs[0], self.inputs[1])] def _is_equivalent_to_binary_commutative(self, other: object) -> bool: return ( diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 683f04dd2..f52e9827c 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -2,7 +2,10 @@ import pytest +from hdk.common.data_types.base import BaseDataType from hdk.common.data_types.dtypes_helpers import ( + find_type_to_hold_both_lossy, + mix_values_determine_holding_dtype, value_is_encrypted_integer, value_is_encrypted_unsigned_integer, ) @@ -53,3 +56,85 @@ def test_value_is_encrypted_integer(value: BaseValue, expected_result: bool): def test_value_is_encrypted_unsigned_integer(value: BaseValue, expected_result: bool): """Test value_is_encrypted_unsigned_integer helper""" assert value_is_encrypted_unsigned_integer(value) == expected_result + + +class UnsupportedDataType(BaseDataType): + """Test helper class to represent an UnsupportedDataType""" + + +@pytest.mark.parametrize( + "dtype1,dtype2,expected_mixed_dtype", + [ + pytest.param(Integer(6, True), Integer(6, True), Integer(6, True), id="int6, int6, int6"), + pytest.param( + Integer(6, False), Integer(6, False), Integer(6, False), id="uint6, uint6, uint6" + ), + pytest.param(Integer(6, True), Integer(6, False), Integer(7, True), id="int6, uint6, int7"), + pytest.param(Integer(6, False), Integer(6, True), Integer(7, True), id="uint6, int6, int7"), + pytest.param(Integer(6, True), Integer(5, False), Integer(6, True), id="int6, uint5, int6"), + pytest.param(Integer(5, False), Integer(6, True), Integer(6, True), id="uint5, int6, int6"), + pytest.param( + UnsupportedDataType(), + UnsupportedDataType(), + None, + id="unsupported, unsupported, xfail", + marks=pytest.mark.xfail(strict=True), + ), + pytest.param( + Integer(6, True), + UnsupportedDataType(), + None, + id="int6, unsupported, xfail", + marks=pytest.mark.xfail(strict=True), + ), + pytest.param( + UnsupportedDataType(), + Integer(6, True), + None, + id="unsupported, int6, xfail", + marks=pytest.mark.xfail(strict=True), + ), + ], +) +def test_mix_data_types( + dtype1: BaseDataType, + dtype2: BaseDataType, + expected_mixed_dtype: BaseDataType, +): + """Test find_type_to_hold_both_lossy helper""" + assert expected_mixed_dtype == find_type_to_hold_both_lossy(dtype1, dtype2) + + +@pytest.mark.parametrize( + "value1,value2,expected_mixed_value", + [ + pytest.param( + EncryptedValue(Integer(7, False)), + EncryptedValue(Integer(7, False)), + EncryptedValue(Integer(7, False)), + id="euint7, euint7, euint7", + ), + pytest.param( + EncryptedValue(Integer(7, False)), + ClearValue(Integer(7, False)), + EncryptedValue(Integer(7, False)), + id="euint7, cuint7, euint7", + ), + pytest.param( + ClearValue(Integer(7, False)), + EncryptedValue(Integer(7, False)), + EncryptedValue(Integer(7, False)), + id="cuint7, euint7, euint7", + ), + pytest.param( + ClearValue(Integer(7, False)), + ClearValue(Integer(7, False)), + ClearValue(Integer(7, False)), + id="cuint7, cuint7, cuint7", + ), + ], +) +def test_mix_values(value1: BaseValue, value2: BaseValue, expected_mixed_value: BaseValue): + """Test mix_values helper""" + + assert expected_mixed_value == mix_values_determine_holding_dtype(value1, value2) From 5e5d0477b12351ff00684ac5b1c82c616fe5a620 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 27 Jul 2021 18:00:29 +0200 Subject: [PATCH 0032/1104] test: add argument annotation in parameter id for clear pytest logs - sometimes arguments are re-ordered in the id written by pytest to sdtout which makes identifying the failing case hard, this solves the issue for one test function --- tests/hnumpy/test_tracing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 07b4acbc1..f9eb2ad3b 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -16,36 +16,36 @@ from hdk.hnumpy import tracing @pytest.mark.parametrize( "x", [ - pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), + pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="x: Encrypted uint"), pytest.param( EncryptedValue(Integer(64, is_signed=True)), - id="Encrypted int", + id="x: Encrypted int", ), pytest.param( ClearValue(Integer(64, is_signed=False)), - id="Clear uint", + id="x: Clear uint", ), pytest.param( ClearValue(Integer(64, is_signed=True)), - id="Clear int", + id="x: Clear int", ), ], ) @pytest.mark.parametrize( "y", [ - pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), + pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="y: Encrypted uint"), pytest.param( EncryptedValue(Integer(64, is_signed=True)), - id="Encrypted int", + id="y: Encrypted int", ), pytest.param( ClearValue(Integer(64, is_signed=False)), - id="Clear uint", + id="y: Clear uint", ), pytest.param( ClearValue(Integer(64, is_signed=True)), - id="Clear int", + id="y: Clear int", ), ], ) From d739e6672ddce300d556a35bcf089827ba561f03 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 28 Jul 2021 11:19:43 +0200 Subject: [PATCH 0033/1104] dev(ordered-inputs): update code to keep the input index in Input IR nodes - this input index will be useful for MLIR/lower level conversions - it represents the input index of an Input node when considering the traced function signature - update code preparing function parameters to keep signature order --- hdk/common/representation/intermediate.py | 3 ++ hdk/common/tracing/__init__.py | 1 + hdk/common/tracing/tracing_helpers.py | 40 ++++++++++++++++---- hdk/hnumpy/tracing.py | 7 +--- tests/common/tracing/test_tracing_helpers.py | 20 +++++++++- tests/hnumpy/test_tracing.py | 4 +- 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index d18612cfa..9e86ed569 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -100,13 +100,16 @@ class Input(IntermediateNode): """Node representing an input of the numpy program""" input_name: str + program_input_idx: int def __init__( self, input_value: BaseValue, input_name: str, + program_input_idx: int, ) -> None: super().__init__((input_value,)) assert len(self.inputs) == 1 self.input_name = input_name + self.program_input_idx = program_input_idx self.outputs = [deepcopy(self.inputs[0])] diff --git a/hdk/common/tracing/__init__.py b/hdk/common/tracing/__init__.py index 1818cb5d9..f311b529e 100644 --- a/hdk/common/tracing/__init__.py +++ b/hdk/common/tracing/__init__.py @@ -3,5 +3,6 @@ from .base_tracer import BaseTracer from .tracing_helpers import ( create_graph_from_output_tracers, make_input_tracer, + make_input_tracers, prepare_function_parameters, ) diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py index e7b1dba42..94bede3ec 100644 --- a/hdk/common/tracing/tracing_helpers.py +++ b/hdk/common/tracing/tracing_helpers.py @@ -1,6 +1,7 @@ """Helper functions for tracing""" +import collections from inspect import signature -from typing import Callable, Dict, Iterable, Set, Tuple, Type +from typing import Callable, Dict, Iterable, OrderedDict, Set, Tuple, Type import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph @@ -10,9 +11,30 @@ from ..representation import intermediate as ir from .base_tracer import BaseTracer +def make_input_tracers( + tracer_class: Type[BaseTracer], + function_parameters: OrderedDict[str, BaseValue], +) -> OrderedDict[str, BaseTracer]: + """Helper function to create tracers for a function's parameters + + Args: + tracer_class (Type[BaseTracer]): the class of tracer to create an Input for + function_parameters (OrderedDict[str, BaseValue]): the dictionary with the parameters names + and corresponding Values + + Returns: + OrderedDict[str, BaseTracer]: the dictionary containing the Input Tracers for each parameter + """ + return collections.OrderedDict( + (param_name, make_input_tracer(tracer_class, param_name, input_idx, param)) + for input_idx, (param_name, param) in enumerate(function_parameters.items()) + ) + + def make_input_tracer( tracer_class: Type[BaseTracer], input_name: str, + input_idx: int, input_value: BaseValue, ) -> BaseTracer: """Helper function to create a tracer for an input value @@ -20,18 +42,19 @@ def make_input_tracer( Args: tracer_class (Type[BaseTracer]): the class of tracer to create an Input for input_name (str): the name of the input in the traced function + input_idx (int): the input index in the function parameters input_value (BaseValue): the Value that is an input and needs to be wrapped in an BaseTracer Returns: BaseTracer: The BaseTracer for that input value """ - return tracer_class([], ir.Input(input_value, input_name), 0) + return tracer_class([], ir.Input(input_value, input_name, input_idx), 0) def prepare_function_parameters( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] -) -> Dict[str, BaseValue]: +) -> OrderedDict[str, BaseValue]: """Function to filter the passed function_parameters to trace function_to_trace Args: @@ -42,7 +65,7 @@ def prepare_function_parameters( ValueError: Raised when some parameters are missing to trace function_to_trace Returns: - Dict[str, BaseValue]: filtered function_parameters dictionary + OrderedDict[str, BaseValue]: filtered function_parameters dictionary """ function_signature = signature(function_to_trace) @@ -54,10 +77,11 @@ def prepare_function_parameters( f"that were not provided: {', '.join(sorted(missing_args))}" ) - useless_arguments = function_parameters.keys() - function_signature.parameters.keys() - useful_arguments = function_signature.parameters.keys() - useless_arguments - - return {k: function_parameters[k] for k in useful_arguments} + # This convoluted way of creating the dict is to ensure key order is maintained + return collections.OrderedDict( + (param_name, function_parameters[param_name]) + for param_name in function_signature.parameters.keys() + ) def create_graph_from_output_tracers( diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index c55d97124..8b10835e1 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -7,7 +7,7 @@ from ..common.data_types import BaseValue from ..common.tracing import ( BaseTracer, create_graph_from_output_tracers, - make_input_tracer, + make_input_tracers, prepare_function_parameters, ) @@ -32,10 +32,7 @@ def trace_numpy_function( """ function_parameters = prepare_function_parameters(function_to_trace, function_parameters) - input_tracers = { - param_name: make_input_tracer(NPTracer, param_name, param) - for param_name, param in function_parameters.items() - } + input_tracers = make_input_tracers(NPTracer, function_parameters) # We could easily create a graph of NPTracer, but we may end up with dead nodes starting from # the inputs that's why we create the graph starting from the outputs diff --git a/tests/common/tracing/test_tracing_helpers.py b/tests/common/tracing/test_tracing_helpers.py index 20adb02ad..38e57a7fc 100644 --- a/tests/common/tracing/test_tracing_helpers.py +++ b/tests/common/tracing/test_tracing_helpers.py @@ -1,6 +1,6 @@ """Test file for HDK's common tracing helpers""" -from typing import Any, Dict +from typing import Any, Dict, List import pytest @@ -24,3 +24,21 @@ def test_prepare_function_parameters( prepared_dict = prepare_function_parameters(function, function_parameters) assert prepared_dict == ref_dict + + +@pytest.mark.parametrize( + "function,function_parameters,expected_ordered_keys", + [ + (lambda x: None, {"x": None}, ["x"]), + (lambda x, y: None, {"x": None, "y": None}, ["x", "y"]), + (lambda x, y: None, {"y": None, "x": None}, ["x", "y"]), + (lambda z, x, y: None, {"y": None, "z": None, "x": None}, ["z", "x", "y"]), + ], +) +def test_prepare_function_parameters_order( + function, function_parameters: Dict[str, Any], expected_ordered_keys: List[str] +): + """Test prepare_function_parameters output order""" + prepared_dict = prepare_function_parameters(function, function_parameters) + + assert list(prepared_dict.keys()) == expected_ordered_keys diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index f9eb2ad3b..46d745374 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -80,8 +80,8 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): ref_graph = nx.MultiDiGraph() - input_x = ir.Input(x, input_name="x") - input_y = ir.Input(y, input_name="y") + input_x = ir.Input(x, input_name="x", program_input_idx=0) + input_y = ir.Input(y, input_name="y", program_input_idx=1) add_node_z = ir.Add( ( From 8925fbd2db64829a39b6fb3a6f9bee9b89e7b76c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 28 Jul 2021 14:44:25 +0200 Subject: [PATCH 0034/1104] dev(ir): add evalute to simulate the computation represented by IR nodes --- hdk/common/representation/intermediate.py | 27 ++++++++++++- .../representation/test_intermediate.py | 40 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/common/representation/test_intermediate.py diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 9e86ed569..a9a6d6c77 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -1,8 +1,8 @@ """File containing HDK's intermdiate representation of source programs operations""" -from abc import ABC +from abc import ABC, abstractmethod from copy import deepcopy -from typing import Any, Dict, Iterable, List, Optional, Tuple +from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple from ..data_types import BaseValue from ..data_types.dtypes_helpers import mix_values_determine_holding_dtype @@ -74,6 +74,17 @@ class IntermediateNode(ABC): and self.op_kwargs == other.op_kwargs ) + @abstractmethod + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + """Function to simulate what the represented computation would output for the given inputs + + Args: + inputs (Mapping[int, Any]): Mapping containing the inputs for the evaluation + + Returns: + Any: the result of the computation + """ + class Add(IntermediateNode): """Addition between two values""" @@ -81,6 +92,9 @@ class Add(IntermediateNode): __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + return inputs[0] + inputs[1] + class Sub(IntermediateNode): """Subtraction between two values""" @@ -88,6 +102,9 @@ class Sub(IntermediateNode): __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_non_commutative + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + return inputs[0] - inputs[1] + class Mul(IntermediateNode): """Multiplication between two values""" @@ -95,6 +112,9 @@ class Mul(IntermediateNode): __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + return inputs[0] * inputs[1] + class Input(IntermediateNode): """Node representing an input of the numpy program""" @@ -113,3 +133,6 @@ class Input(IntermediateNode): self.input_name = input_name self.program_input_idx = program_input_idx self.outputs = [deepcopy(self.inputs[0])] + + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + return inputs[0] diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py new file mode 100644 index 000000000..d2956eb9a --- /dev/null +++ b/tests/common/representation/test_intermediate.py @@ -0,0 +1,40 @@ +"""Test file for HDK's common/representation/intermediate.py""" + +import pytest + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearValue, EncryptedValue +from hdk.common.representation import intermediate as ir + + +@pytest.mark.parametrize( + "node,input_data,expected_result", + [ + pytest.param( + ir.Add([EncryptedValue(Integer(64, False)), EncryptedValue(Integer(64, False))]), + [10, 4589], + 4599, + id="Add", + ), + pytest.param( + ir.Sub([EncryptedValue(Integer(64, False)), EncryptedValue(Integer(64, False))]), + [10, 4589], + -4579, + id="Sub", + ), + pytest.param( + ir.Mul([EncryptedValue(Integer(64, False)), EncryptedValue(Integer(64, False))]), + [10, 4589], + 45890, + id="Mul", + ), + pytest.param(ir.Input(ClearValue(Integer(32, True)), "in", 0), [42], 42, id="Input"), + ], +) +def test_evaluate( + node: ir.IntermediateNode, + input_data, + expected_result: int, +): + """Test evaluate methods on IntermediateNodes""" + assert node.evaluate(input_data) == expected_result From 1196b00c6bef96cc6bf269b9495b36dc61945d52 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 28 Jul 2021 17:38:27 +0200 Subject: [PATCH 0035/1104] feat(debugging): implementing draw_graph draw_graph is the function to show on a plot the traced function-to-compile add edge numbers refs #38 --- hdk/common/__init__.py | 2 +- hdk/common/debugging/__init__.py | 2 + hdk/common/debugging/draw_graph.py | 196 +++++++++++++++++++++++++++++ poetry.lock | 7 +- pyproject.toml | 1 + tests/hnumpy/test_debugging.py | 58 +++++++++ 6 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 hdk/common/debugging/__init__.py create mode 100644 hdk/common/debugging/draw_graph.py create mode 100644 tests/hnumpy/test_debugging.py diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index cd563aaf9..48c4420ae 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,2 +1,2 @@ """HDK's module for shared data structures and code""" -from . import data_types, representation +from . import data_types, debugging, representation diff --git a/hdk/common/debugging/__init__.py b/hdk/common/debugging/__init__.py new file mode 100644 index 000000000..b18d821bf --- /dev/null +++ b/hdk/common/debugging/__init__.py @@ -0,0 +1,2 @@ +"""HDK's module for debugging""" +from .draw_graph import draw_graph diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py new file mode 100644 index 000000000..46474d0b7 --- /dev/null +++ b/hdk/common/debugging/draw_graph.py @@ -0,0 +1,196 @@ +"""functions to draw the different graphs we can generate in the package, eg to debug""" +from typing import Dict, List + +import matplotlib.pyplot as plt +import networkx as nx + +from hdk.common.representation import intermediate as ir + +IR_NODE_COLOR_MAPPING = {ir.Input: "blue", ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green"} + + +def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float = 1.0) -> Dict: + """ + Returns a pos to be used later with eg nx.draw_networkx_nodes, so that nodes + are ordered by depth from input along the x axis and have a uniform + distribution along the y axis + + Args: + graph (nx.Graph): The graph that we want to draw + x_delta (float): Parameter used to set the increment in x + y_delta (float): Parameter used to set the increment in y + + Returns: + pos (Dict): the argument to use with eg nx.draw_networkx_nodes + + """ + + # FIXME: less variables + # pylint: disable=too-many-locals + + nodes_depth = {node: 0 for node in graph.nodes()} + input_nodes = [node for node in graph.nodes() if len(list(graph.predecessors(node))) == 0] + + # Init a layout so that unreachable nodes have a pos, avoids potential crashes wiht networkx + # use a cheap layout + pos = nx.random_layout(graph) + + curr_x = 0.0 + curr_y = -(len(input_nodes) - 1) / 2 * y_delta + + for in_node in input_nodes: + pos[in_node] = (curr_x, curr_y) + curr_y += y_delta + + curr_x += x_delta + + curr_nodes = input_nodes + + current_depth = 0 + while len(curr_nodes) > 0: + current_depth += 1 + next_nodes_set = set() + for node in curr_nodes: + next_nodes_set.update(graph.successors(node)) + + curr_nodes = list(next_nodes_set) + for node in curr_nodes: + nodes_depth[node] = current_depth + + nodes_by_depth: Dict[int, List[int]] = {} + for node, depth in nodes_depth.items(): + nodes_for_depth = nodes_by_depth.get(depth, []) + nodes_for_depth.append(node) + nodes_by_depth[depth] = nodes_for_depth + + depths = sorted(nodes_by_depth.keys()) + + for depth in depths: + nodes_at_depth = nodes_by_depth[depth] + + curr_y = -(len(nodes_at_depth) - 1) / 2 * y_delta + for node in nodes_at_depth: + pos[node] = (curr_x, curr_y) + curr_y += y_delta + + curr_x += x_delta + + # pylint: enable=too-many-locals + return pos + + +def draw_graph( + graph: nx.DiGraph, block_until_user_closes_graph: bool = True, draw_edge_numbers: bool = True +) -> None: + """ + Draw a graph + + Args: + graph (nx.DiGraph): The graph that we want to draw + block_until_user_closes_graph (bool): if True, will wait the user to + close the figure before continuing; False is useful for the CI tests + draw_edge_numbers (bool): if True, add the edge number on the arrow + linking nodes, eg to differentiate the x and y in a Sub coding + (x - y). This option is not that useful for commutative ops, and + may make the picture a bit too dense, so could be deactivated + + Returns: + None + + """ + + # FIXME: less variables + # pylint: disable=too-many-locals + + # Positions of the node + pos = human_readable_layout(graph) + + # Colors and labels + color_map = [IR_NODE_COLOR_MAPPING[type(node)] for node in graph.nodes()] + + # For most types, we just pick the operation as the label, but for Input, + # we take the name of the variable, ie the argument name of the function + # to compile + label_dict = { + node: node.input_name if isinstance(node, ir.Input) else node.__class__.__name__ + for node in graph.nodes() + } + + # Draw nodes + nx.draw_networkx_nodes( + graph, + pos, + node_color=color_map, + node_size=1000, + alpha=1, + ) + + # Draw labels + nx.draw_networkx_labels(graph, pos, labels=label_dict) + + current_axes = plt.gca() + + # And draw edges in a way which works when we have two "equivalent edges", + # ie from the same node A to the same node B, like to represent y = x + x + already_done = set() + + for e in graph.edges: + + # If we already drew the different edges from e[0] to e[1], continue + if (e[0], e[1]) in already_done: + continue + + already_done.add((e[0], e[1])) + + edges = graph.get_edge_data(e[0], e[1]) + + # Draw the different edges from e[0] to e[1], continue + for which, edge in enumerate(edges.values()): + edge_index = edge["input_idx"] + + # Draw the edge + current_axes.annotate( + "", + xy=pos[e[0]], + xycoords="data", + xytext=pos[e[1]], + textcoords="data", + arrowprops=dict( + arrowstyle="<-", + color="0.5", + shrinkA=5, + shrinkB=5, + patchA=None, + patchB=None, + connectionstyle="arc3,rad=rrr".replace("rrr", str(0.3 * which)), + ), + ) + + if draw_edge_numbers: + # Print the number of the node on the edge. This is a bit artisanal, + # since it seems not possible to add the text directly on the + # previously drawn arrow. So, more or less, we try to put a text at + # a position which is close to pos[e[1]] and which varies a bit with + # 'which' + a, b = pos[e[0]] + c, d = pos[e[1]] + const_0 = 1 + const_1 = 2 + + current_axes.annotate( + str(edge_index), + xycoords="data", + xy=( + (const_0 * a + const_1 * c) / (const_0 + const_1), + (const_0 * b + const_1 * d + 0.1 * which) / (const_0 + const_1), + ), + textcoords="data", + ) + + plt.axis("off") + + # block_until_user_closes_graph is used as True for real users and False + # for CI + plt.show(block=block_until_user_closes_graph) + + # pylint: enable=too-many-locals diff --git a/poetry.lock b/poetry.lock index c0eb2c77b..2337759c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -821,7 +821,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "2dbebb6e1d3b5f35cd48891bb9ed49b9d57d1f5659419e27150c5a3786d4b054" +content-hash = "e0f2368e94fc45bae93f9695b3702a12f4d2c013caaff22012e3abfc4f7af6ab" [metadata.files] alabaster = [ @@ -1199,6 +1199,11 @@ pathspec = [ {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] pillow = [ + {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, + {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, + {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, + {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, + {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, diff --git a/pyproject.toml b/pyproject.toml index 13c80cb78..0b1603c32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ Sphinx = "^4.1.1" sphinx-rtd-theme = "^0.5.2" myst-parser = "^0.15.1" networkx = "^2.6.1" +matplotlib = "^3.4.2" [tool.poetry.dev-dependencies] isort = "^5.9.2" diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py new file mode 100644 index 000000000..2ade31562 --- /dev/null +++ b/tests/hnumpy/test_debugging.py @@ -0,0 +1,58 @@ +"""Test file for HDK's hnumpy debugging functions""" + +import pytest + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearValue, EncryptedValue +from hdk.common.debugging import draw_graph +from hdk.hnumpy import tracing + + +@pytest.mark.parametrize( + "lambda_f", + [ + lambda x, y: x + y, + lambda x, y: x + x - y * y * y + x, + ], +) +@pytest.mark.parametrize( + "x", + [ + pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), + # pytest.param( + # EncryptedValue(Integer(64, is_signed=True)), + # id="Encrypted int", + # ), + # pytest.param( + # ClearValue(Integer(64, is_signed=False)), + # id="Clear uint", + # ), + # pytest.param( + # ClearValue(Integer(64, is_signed=True)), + # id="Clear int", + # ), + ], +) +@pytest.mark.parametrize( + "y", + [ + pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), + # pytest.param( + # EncryptedValue(Integer(64, is_signed=True)), + # id="Encrypted int", + # ), + pytest.param( + ClearValue(Integer(64, is_signed=False)), + id="Clear uint", + ), + # pytest.param( + # ClearValue(Integer(64, is_signed=True)), + # id="Clear int", + # ), + ], +) +def test_hnumpy_draw_graph(lambda_f, x, y): + "Test hnumpy draw_graph" + graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) + + draw_graph(graph, block_until_user_closes_graph=False) From c91ee858c50af42fa79358ad490f62f3b525c440 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 29 Jul 2021 17:59:03 +0200 Subject: [PATCH 0036/1104] feat: adding a function to print a graph refs #38 --- hdk/common/debugging/__init__.py | 2 +- hdk/common/debugging/draw_graph.py | 51 ++++++++++++++++++++++- tests/hnumpy/test_debugging.py | 65 ++++++++++++++---------------- 3 files changed, 81 insertions(+), 37 deletions(-) diff --git a/hdk/common/debugging/__init__.py b/hdk/common/debugging/__init__.py index b18d821bf..5be00bb60 100644 --- a/hdk/common/debugging/__init__.py +++ b/hdk/common/debugging/__init__.py @@ -1,2 +1,2 @@ """HDK's module for debugging""" -from .draw_graph import draw_graph +from .draw_graph import draw_graph, get_printable_graph diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index 46474d0b7..b012da781 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -1,5 +1,5 @@ """functions to draw the different graphs we can generate in the package, eg to debug""" -from typing import Dict, List +from typing import Any, Dict, List import matplotlib.pyplot as plt import networkx as nx @@ -194,3 +194,52 @@ def draw_graph( plt.show(block=block_until_user_closes_graph) # pylint: enable=too-many-locals + + +def get_printable_graph(graph: nx.DiGraph) -> str: + """ + Return a string representing a graph + + Args: + graph (nx.DiGraph): The graph that we want to draw + + Returns: + a string to print or save in a file + + """ + returned_str = "" + + i = 0 + map_table: Dict[Any, int] = {} + + for node in nx.topological_sort(graph): + + if not isinstance(node, ir.Input): + what_to_print = node.__class__.__name__ + "(" + + # Find all the names of the current predecessors of the node + list_of_arg_name = [] + + for pred, index_list in graph.pred[node].items(): + for index in index_list.values(): + # Remark that we keep the index of the predecessor and its + # name, to print sources in the right order, which is + # important for eg non commutative operations + list_of_arg_name += [(index["input_idx"], str(map_table[pred]))] + + # Some checks, because the previous algorithm is not clear + assert len(list_of_arg_name) == len({x[0] for x in list_of_arg_name}) + assert [x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name))) + + # Then, just print the predecessors in the right order + list_of_arg_name.sort() + what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" + + else: + what_to_print = node.input_name + + returned_str += f"\n%{i} = {what_to_print}" + map_table[node] = i + i += 1 + + return returned_str diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 2ade31562..1fe0d547b 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -4,55 +4,50 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import ClearValue, EncryptedValue -from hdk.common.debugging import draw_graph +from hdk.common.debugging import draw_graph, get_printable_graph from hdk.hnumpy import tracing @pytest.mark.parametrize( - "lambda_f", + "lambda_f,ref_graph_str", [ - lambda x, y: x + y, - lambda x, y: x + x - y * y * y + x, + (lambda x, y: x + y, "\n%0 = x\n%1 = y\n%2 = Add(0, 1)"), + (lambda x, y: x - y, "\n%0 = x\n%1 = y\n%2 = Sub(0, 1)"), + ( + lambda x, y: x + x - y * y * y + x, + "\n%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" + "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)", + ), ], ) @pytest.mark.parametrize( - "x", + "x_y", [ - pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), - # pytest.param( - # EncryptedValue(Integer(64, is_signed=True)), - # id="Encrypted int", - # ), - # pytest.param( - # ClearValue(Integer(64, is_signed=False)), - # id="Clear uint", - # ), - # pytest.param( - # ClearValue(Integer(64, is_signed=True)), - # id="Clear int", - # ), - ], -) -@pytest.mark.parametrize( - "y", - [ - pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="Encrypted uint"), - # pytest.param( - # EncryptedValue(Integer(64, is_signed=True)), - # id="Encrypted int", - # ), pytest.param( - ClearValue(Integer(64, is_signed=False)), + ( + EncryptedValue(Integer(64, is_signed=False)), + EncryptedValue(Integer(64, is_signed=False)), + ), + id="Encrypted uint", + ), + pytest.param( + ( + EncryptedValue(Integer(64, is_signed=False)), + ClearValue(Integer(64, is_signed=False)), + ), id="Clear uint", ), - # pytest.param( - # ClearValue(Integer(64, is_signed=True)), - # id="Clear int", - # ), ], ) -def test_hnumpy_draw_graph(lambda_f, x, y): - "Test hnumpy draw_graph" +def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): + "Test hnumpy get_printable_graph and draw_graph" + x, y = x_y graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) draw_graph(graph, block_until_user_closes_graph=False) + + str_of_the_graph = get_printable_graph(graph) + + print(f"\n{str_of_the_graph}\n") + + assert str_of_the_graph == ref_graph_str From be391ca3887b99987d395c48611393532c112053 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 28 Jul 2021 15:58:10 +0200 Subject: [PATCH 0037/1104] chore: remove reference to package name and file paths - avoids desync with the package name and file paths (which are unstable) --- hdk/__init__.py | 2 +- hdk/common/__init__.py | 2 +- hdk/common/data_types/__init__.py | 2 +- hdk/common/debugging/__init__.py | 2 +- hdk/common/representation/__init__.py | 2 +- hdk/common/representation/intermediate.py | 2 +- hdk/common/tracing/__init__.py | 2 +- hdk/hnumpy/__init__.py | 2 +- tests/common/data_types/test_dtypes_helpers.py | 2 +- tests/common/data_types/test_integers.py | 2 +- tests/common/data_types/test_values.py | 2 +- tests/common/representation/test_intermediate.py | 2 +- tests/common/tracing/test_tracing_helpers.py | 2 +- tests/hnumpy/test_debugging.py | 2 +- tests/hnumpy/test_tracing.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hdk/__init__.py b/hdk/__init__.py index a121a3f0c..7189e5b42 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -1,2 +1,2 @@ -"""HDK's top import""" +"""Package top import""" from . import common, hnumpy diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index 48c4420ae..eecdb8723 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,2 +1,2 @@ -"""HDK's module for shared data structures and code""" +"""Module for shared data structures and code""" from . import data_types, debugging, representation diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index 5c2244e21..54bbbd739 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,3 +1,3 @@ -"""HDK's module for data types code and data structures""" +"""Module for data types code and data structures""" from . import dtypes_helpers, integers, values from .values import BaseValue diff --git a/hdk/common/debugging/__init__.py b/hdk/common/debugging/__init__.py index 5be00bb60..16dbae9be 100644 --- a/hdk/common/debugging/__init__.py +++ b/hdk/common/debugging/__init__.py @@ -1,2 +1,2 @@ -"""HDK's module for debugging""" +"""Module for debugging""" from .draw_graph import draw_graph, get_printable_graph diff --git a/hdk/common/representation/__init__.py b/hdk/common/representation/__init__.py index 7bdfcc4df..523d9963f 100644 --- a/hdk/common/representation/__init__.py +++ b/hdk/common/representation/__init__.py @@ -1,2 +1,2 @@ -"""HDK's representation module to represent source programs""" +"""Representation module to represent source programs""" from . import intermediate diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index a9a6d6c77..9bc0ffddb 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -1,4 +1,4 @@ -"""File containing HDK's intermdiate representation of source programs operations""" +"""File containing code to represent source programs operations""" from abc import ABC, abstractmethod from copy import deepcopy diff --git a/hdk/common/tracing/__init__.py b/hdk/common/tracing/__init__.py index f311b529e..ac51d4934 100644 --- a/hdk/common/tracing/__init__.py +++ b/hdk/common/tracing/__init__.py @@ -1,4 +1,4 @@ -"""HDK's module for basic tracing facilities""" +"""Module for basic tracing facilities""" from .base_tracer import BaseTracer from .tracing_helpers import ( create_graph_from_output_tracers, diff --git a/hdk/hnumpy/__init__.py b/hdk/hnumpy/__init__.py index 5af83dc23..ddbd6bf7c 100644 --- a/hdk/hnumpy/__init__.py +++ b/hdk/hnumpy/__init__.py @@ -1,2 +1,2 @@ -"""HDK's module for compiling numpy functions to homomorphic equivalents""" +"""Module for compiling numpy functions to homomorphic equivalents""" from . import tracing diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index f52e9827c..d9fc88099 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -1,4 +1,4 @@ -"""Test file for HDK's data types helpers""" +"""Test file for data types helpers""" import pytest diff --git a/tests/common/data_types/test_integers.py b/tests/common/data_types/test_integers.py index 5780cf995..332db50df 100644 --- a/tests/common/data_types/test_integers.py +++ b/tests/common/data_types/test_integers.py @@ -1,4 +1,4 @@ -"""Test file for HDK's common/data_types/integers.py""" +"""Test file for integers data types""" import random diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py index de9803d62..07dbf7462 100644 --- a/tests/common/data_types/test_values.py +++ b/tests/common/data_types/test_values.py @@ -1,4 +1,4 @@ -"""Test file for HDK's common/data_types/values.py""" +"""Test file for values classes""" import pytest diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index d2956eb9a..ba8e8a629 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -1,4 +1,4 @@ -"""Test file for HDK's common/representation/intermediate.py""" +"""Test file for intermediate representation""" import pytest diff --git a/tests/common/tracing/test_tracing_helpers.py b/tests/common/tracing/test_tracing_helpers.py index 38e57a7fc..36996cb47 100644 --- a/tests/common/tracing/test_tracing_helpers.py +++ b/tests/common/tracing/test_tracing_helpers.py @@ -1,4 +1,4 @@ -"""Test file for HDK's common tracing helpers""" +"""Test file for common tracing helpers""" from typing import Any, Dict, List diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 1fe0d547b..5faf2fe6f 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -1,4 +1,4 @@ -"""Test file for HDK's hnumpy debugging functions""" +"""Test file for hnumpy debugging functions""" import pytest diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 46d745374..5383bb3b3 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -1,4 +1,4 @@ -"""Test file for HDK's hnumpy tracing""" +"""Test file for hnumpy tracing""" import networkx as nx import pytest From 9b52ea94fb4c945cba69a6eea6b6c480b9448920 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 30 Jul 2021 15:03:59 +0200 Subject: [PATCH 0038/1104] dev(opgraph): add a class to ease manipulating an operator graph --- hdk/common/debugging/draw_graph.py | 26 ++++++++----- hdk/common/operator_graph.py | 59 ++++++++++++++++++++++++++++++ hdk/hnumpy/tracing.py | 20 ++++------ tests/hnumpy/test_tracing.py | 4 +- 4 files changed, 85 insertions(+), 24 deletions(-) create mode 100644 hdk/common/operator_graph.py diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index b012da781..e91623a04 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -1,9 +1,10 @@ """functions to draw the different graphs we can generate in the package, eg to debug""" -from typing import Any, Dict, List +from typing import Any, Dict, List, Union import matplotlib.pyplot as plt import networkx as nx +from hdk.common.operator_graph import OPGraph from hdk.common.representation import intermediate as ir IR_NODE_COLOR_MAPPING = {ir.Input: "blue", ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green"} @@ -80,13 +81,15 @@ def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float def draw_graph( - graph: nx.DiGraph, block_until_user_closes_graph: bool = True, draw_edge_numbers: bool = True + graph: Union[OPGraph, nx.MultiDiGraph], + block_until_user_closes_graph: bool = True, + draw_edge_numbers: bool = True, ) -> None: """ Draw a graph Args: - graph (nx.DiGraph): The graph that we want to draw + graph (Union[OPGraph, nx.MultiDiGraph]): The graph that we want to draw block_until_user_closes_graph (bool): if True, will wait the user to close the figure before continuing; False is useful for the CI tests draw_edge_numbers (bool): if True, add the edge number on the arrow @@ -102,6 +105,9 @@ def draw_graph( # FIXME: less variables # pylint: disable=too-many-locals + # Allow to pass either OPGraph or an nx graph, manage this here + graph = graph.graph if isinstance(graph, OPGraph) else graph + # Positions of the node pos = human_readable_layout(graph) @@ -196,17 +202,19 @@ def draw_graph( # pylint: enable=too-many-locals -def get_printable_graph(graph: nx.DiGraph) -> str: - """ - Return a string representing a graph +def get_printable_graph(graph: Union[OPGraph, nx.MultiDiGraph]) -> str: + """Return a string representing a graph Args: - graph (nx.DiGraph): The graph that we want to draw + graph (Union[OPGraph, nx.MultiDiGraph]): The graph that we want to draw Returns: - a string to print or save in a file - + str: a string to print or save in a file """ + + # Allow to pass either OPGraph or an nx graph, manage this here + graph = graph.graph if isinstance(graph, OPGraph) else graph + returned_str = "" i = 0 diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py new file mode 100644 index 000000000..ccb7500a4 --- /dev/null +++ b/hdk/common/operator_graph.py @@ -0,0 +1,59 @@ +"""Code to wrap and make manipulating networkx graphs easier""" + +from typing import Any, Dict, Iterable, Mapping + +import networkx as nx + +from .representation import intermediate as ir +from .tracing import BaseTracer +from .tracing.tracing_helpers import create_graph_from_output_tracers + + +class OPGraph: + """Class to make work with nx graphs easier""" + + graph: nx.MultiDiGraph + input_nodes: Mapping[int, ir.Input] + output_nodes: Mapping[int, ir.IntermediateNode] + + def __init__(self, output_tracers: Iterable[BaseTracer]) -> None: + self.output_nodes = { + output_idx: tracer.traced_computation + for output_idx, tracer in enumerate(output_tracers) + } + self.graph = create_graph_from_output_tracers(output_tracers) + self.input_nodes = { + node.program_input_idx: node + for node in self.graph.nodes() + if len(self.graph.pred[node]) == 0 + } + + assert all(map(lambda x: isinstance(x, ir.Input), self.input_nodes.values())) + + graph_outputs = set(node for node in self.graph.nodes() if len(self.graph.succ[node]) == 0) + + assert set(self.output_nodes.values()) == graph_outputs + + def evaluate(self, inputs: Mapping[int, Any]) -> Dict[ir.IntermediateNode, Any]: + """Function to evaluate a graph and get intermediate values for all nodes + + Args: + inputs (Mapping[int, Any]): The inputs to the program + + Returns: + Dict[ir.IntermediateNode, Any]: Dictionary with node as keys and resulting values + """ + node_results: Dict[ir.IntermediateNode, Any] = {} + + for node in nx.topological_sort(self.graph): + if not isinstance(node, ir.Input): + curr_inputs = {} + for pred_node in self.graph.pred[node]: + edges = self.graph.get_edge_data(pred_node, node) + for edge in edges.values(): + curr_inputs[edge["input_idx"]] = node_results[pred_node] + node_results[node] = node.evaluate(curr_inputs) + else: + node_results[node] = node.evaluate({0: inputs[node.program_input_idx]}) + + return node_results diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 8b10835e1..8072fdc18 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,15 +1,9 @@ """hnumpy tracing utilities""" from typing import Callable, Dict -import networkx as nx - from ..common.data_types import BaseValue -from ..common.tracing import ( - BaseTracer, - create_graph_from_output_tracers, - make_input_tracers, - prepare_function_parameters, -) +from ..common.operator_graph import OPGraph +from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters class NPTracer(BaseTracer): @@ -18,7 +12,7 @@ class NPTracer(BaseTracer): def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] -) -> nx.MultiDiGraph: +) -> OPGraph: """Function used to trace a numpy function Args: @@ -27,8 +21,8 @@ def trace_numpy_function( function is e.g. an EncryptedValue holding a 7bits unsigned Integer Returns: - nx.MultiDiGraph: The graph containing the ir nodes representing the computation done in the - input function + OPGraph: The graph containing the ir nodes representing the computation done in the input + function """ function_parameters = prepare_function_parameters(function_to_trace, function_parameters) @@ -40,6 +34,6 @@ def trace_numpy_function( if isinstance(output_tracers, NPTracer): output_tracers = (output_tracers,) - graph = create_graph_from_output_tracers(output_tracers) + op_graph = OPGraph(output_tracers) - return graph + return op_graph diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 5383bb3b3..4dfada2d6 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -76,7 +76,7 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): else: assert False, f"unknown operation {operation}" - graph = tracing.trace_numpy_function(function_to_compile, {"x": x, "y": y}) + op_graph = tracing.trace_numpy_function(function_to_compile, {"x": x, "y": y}) ref_graph = nx.MultiDiGraph() @@ -108,4 +108,4 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): ref_graph.add_edge(add_node_z, returned_final_node, input_idx=0) ref_graph.add_edge(input_y, returned_final_node, input_idx=1) - assert test_helpers.digraphs_are_equivalent(ref_graph, graph) + assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) From e55284b3ea0d028fcf684883522be435c450e49d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 29 Jul 2021 15:49:04 +0200 Subject: [PATCH 0039/1104] feat(bounds): add a way to evaluate an operator graph on a dataset --- hdk/common/bounds_measurement/__init__.py | 2 + hdk/common/bounds_measurement/dataset_eval.py | 34 ++++++ .../bounds_measurement/test_dataset_eval.py | 101 ++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 hdk/common/bounds_measurement/__init__.py create mode 100644 hdk/common/bounds_measurement/dataset_eval.py create mode 100644 tests/common/bounds_measurement/test_dataset_eval.py diff --git a/hdk/common/bounds_measurement/__init__.py b/hdk/common/bounds_measurement/__init__.py new file mode 100644 index 000000000..00836be57 --- /dev/null +++ b/hdk/common/bounds_measurement/__init__.py @@ -0,0 +1,2 @@ +"""Bounds measurement module""" +from . import dataset_eval diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/hdk/common/bounds_measurement/dataset_eval.py new file mode 100644 index 000000000..d30dda021 --- /dev/null +++ b/hdk/common/bounds_measurement/dataset_eval.py @@ -0,0 +1,34 @@ +"""Code to evaluate the IR graph on datasets""" + +from typing import Iterator + +from ..operator_graph import OPGraph + + +def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, data_generator: Iterator): + """Evaluate the bounds for all output values of the operators in the graph op_graph over data + coming from the data_generator + + Args: + op_graph (OPGraph): The graph for which we want to determine the bounds + data_generator (Iterator): The dataset over which op_graph is evaluated + + Returns: + Dict: dict containing the bounds for each node from op_graph, stored with the node as key + and a dict with keys "min" and "max" as value + """ + first_input_data = dict(enumerate(next(data_generator))) + first_output = op_graph.evaluate(first_input_data) + + node_bounds = { + node: {"min": first_output[node], "max": first_output[node]} + for node in op_graph.graph.nodes() + } + + for input_data in data_generator: + current_output = op_graph.evaluate(dict(enumerate(input_data))) + for node, value in current_output.items(): + node_bounds[node]["min"] = min(node_bounds[node]["min"], value) + node_bounds[node]["max"] = max(node_bounds[node]["max"], value) + + return node_bounds diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py new file mode 100644 index 000000000..7c82bf6e7 --- /dev/null +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -0,0 +1,101 @@ +"""Test file for bounds evaluation with a dataset""" + +import pytest + +from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.hnumpy.tracing import trace_numpy_function + + +@pytest.mark.parametrize( + "function,input_ranges,expected_output_bounds", + [ + pytest.param( + lambda x, y: x + y, + ((-10, 10), (-10, 10)), + (-20, 20), + id="x + y, (-10, 10), (-10, 10), (-20, 20)", + ), + pytest.param( + lambda x, y: x + y, + ((-10, 2), (-4, 5)), + (-14, 7), + id="x + y, (-10, 2), (-4, 5), (-14, 9)", + ), + pytest.param( + lambda x, y: x - y, + ((-10, 10), (-10, 10)), + (-20, 20), + id="x - y, (-10, 10), (-10, 10), (-20, 20)", + ), + pytest.param( + lambda x, y: x - y, + ((-10, 2), (-4, 5)), + (-15, 6), + id="x - y, (-10, 2), (-4, 5), (-15, 6)", + ), + pytest.param( + lambda x, y: x * y, + ((-10, 10), (-10, 10)), + (-100, 100), + id="x * y, (-10, 10), (-10, 10), (-100, 100)", + ), + pytest.param( + lambda x, y: x * y, + ((-10, 2), (-4, 5)), + (-50, 40), + id="x * y, (-10, 2), (-4, 5), (-50, 40)", + ), + pytest.param( + lambda x, y: x + x + y, + ((-10, 10), (-10, 10)), + (-30, 30), + id="x + x + y, (-10, 10), (-10, 10), (-30, 30)", + ), + pytest.param( + lambda x, y: x - x + y, + ((-10, 10), (-10, 10)), + (-10, 10), + id="x - x + y, (-10, 10), (-10, 10), (-10, 10)", + ), + pytest.param( + lambda x, y: x - x + y, + ((-10, 2), (-4, 5)), + (-4, 5), + id="x - x + y, (-10, 2), (-4, 5), (-4, 5)", + ), + pytest.param( + lambda x, y: x * y - x, + ((-10, 10), (-10, 10)), + (-110, 110), + id="x * y - x, (-10, 10), (-10, 10), (-110, 110)", + ), + pytest.param( + lambda x, y: x * y - x, + ((-10, 2), (-4, 5)), + (-40, 50), + id="x * y - x, (-10, 2), (-4, 5), (-40, 50),", + ), + ], +) +def test_eval_op_graph_bounds_on_dataset(function, input_ranges, expected_output_bounds): + """Test function for eval_op_graph_bounds_on_dataset""" + + op_graph = trace_numpy_function( + function, {"x": EncryptedValue(Integer(64, True)), "y": EncryptedValue(Integer(64, True))} + ) + + def data_gen(range_x, range_y): + for x_gen in range_x: + for y_gen in range_y: + yield (x_gen, y_gen) + + node_bounds = eval_op_graph_bounds_on_dataset( + op_graph, data_gen(*tuple(map(lambda x: range(x[0], x[1] + 1), input_ranges))) + ) + + output_node = op_graph.output_nodes[0] + output_node_bounds = node_bounds[output_node] + + assert (output_node_bounds["min"], output_node_bounds["max"]) == expected_output_bounds From b1a3b28a20e91588ba1d08d98be75c99be0fb628 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 30 Jul 2021 14:14:34 +0200 Subject: [PATCH 0040/1104] dev(dtypes): add a function to make Integers able to hold a set of values --- hdk/common/data_types/integers.py | 60 ++++++++++++++++++++++++ tests/common/data_types/test_integers.py | 44 ++++++++++++++++- 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index 91b34d992..49c04e23f 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -1,5 +1,8 @@ """This file holds the definitions for integer types""" +import math +from typing import Iterable + from . import base @@ -79,3 +82,60 @@ def create_unsigned_integer(bit_width: int) -> Integer: UnsignedInteger = create_unsigned_integer + + +def make_integer_to_hold_ints(values: Iterable[int], force_signed: bool) -> Integer: + """Returns an Integer able to hold all values, it is possible to force the Integer to be signed + + Args: + values (Iterable[int]): The values to hold + force_signed (bool): Set to True to force the result to be a signed Integer + + Returns: + Integer: The Integer able to hold values + """ + assert all(map(lambda x: isinstance(x, int), values)) + min_value = min(values) + max_value = max(values) + + make_signed_integer = force_signed or min_value < 0 + + num_bits = max( + get_bits_to_represent_int(min_value, make_signed_integer), + get_bits_to_represent_int(max_value, make_signed_integer), + ) + + return Integer(num_bits, is_signed=make_signed_integer) + + +def get_bits_to_represent_int(value: int, force_signed: bool) -> int: + """Returns how many bits are required to represent a single int + + Args: + value (int): The int for which we want to know how many bits are required + force_signed (bool): Set to True to force the result to be a signed Integer + + Returns: + int: required amount of bits + """ + + # Writing this in a very dumb way + num_bits: int + if value < 0: + abs_value = abs(value) + if abs_value > 1: + num_bits = math.ceil(math.log2(abs_value)) + 1 + else: + # -1 case + num_bits = 2 + else: + if value > 1: + num_bits = math.ceil(math.log2(value + 1)) + else: + # 0 and 1 case + num_bits = 1 + + if force_signed: + num_bits += 1 + + return num_bits diff --git a/tests/common/data_types/test_integers.py b/tests/common/data_types/test_integers.py index 332db50df..c4ed8f1fc 100644 --- a/tests/common/data_types/test_integers.py +++ b/tests/common/data_types/test_integers.py @@ -4,7 +4,12 @@ import random import pytest -from hdk.common.data_types.integers import Integer, SignedInteger, UnsignedInteger +from hdk.common.data_types.integers import ( + Integer, + SignedInteger, + UnsignedInteger, + make_integer_to_hold_ints, +) @pytest.mark.parametrize( @@ -68,3 +73,40 @@ def test_basic_integers(integer: Integer, expected_min: int, expected_max: int): def test_integers_repr(integer: Integer, expected_repr_str: str): """Test integer repr""" assert integer.__repr__() == expected_repr_str + + +@pytest.mark.parametrize( + "values,force_signed,expected_result", + [ + ([0], False, Integer(1, is_signed=False)), + ([0], True, Integer(2, is_signed=True)), + ([1], False, Integer(1, is_signed=False)), + ([1], True, Integer(2, is_signed=True)), + ([-1], False, Integer(2, is_signed=True)), + ([-2], False, Integer(2, is_signed=True)), + ([0, 1], False, Integer(1, is_signed=False)), + ([0, 1], True, Integer(2, is_signed=True)), + ([7], False, Integer(3, is_signed=False)), + ([7], True, Integer(4, is_signed=True)), + ([8], False, Integer(4, is_signed=False)), + ([8], True, Integer(5, is_signed=True)), + ([-7], False, Integer(4, is_signed=True)), + ([-8], False, Integer(4, is_signed=True)), + ([-7, -8], False, Integer(4, is_signed=True)), + ([-9], False, Integer(5, is_signed=True)), + ([-9], True, Integer(5, is_signed=True)), + ([0, 127], False, Integer(7, is_signed=False)), + ([0, 127], True, Integer(8, is_signed=True)), + ([0, 128], False, Integer(8, is_signed=False)), + ([0, 128], True, Integer(9, is_signed=True)), + ([-1, 127], False, Integer(8, is_signed=True)), + ([-256, 127], False, Integer(9, is_signed=True)), + ([-128, 127], False, Integer(8, is_signed=True)), + ([-128, 128], False, Integer(9, is_signed=True)), + ([-13, 4], False, Integer(5, is_signed=True)), + ([42, 1019], False, Integer(10, is_signed=False)), + ], +) +def test_make_integer_to_hold(values, force_signed, expected_result): + """Test make_integer_to_hold""" + assert expected_result == make_integer_to_hold_ints(values, force_signed) From a158b09f44d24974cec74d3b3746e982b16b20bc Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 2 Aug 2021 10:27:30 +0200 Subject: [PATCH 0041/1104] feat(bounds): add function to update OPGraph IR nodes in and output values - this allows to have tighter data types by sticking to the smallest types able to represent the ranges passed as argument - update test_dataset_eval to check the output Value's data_type is updated --- hdk/common/operator_graph.py | 39 +++++++++++++++++++ .../bounds_measurement/test_dataset_eval.py | 25 +++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index ccb7500a4..b1ad61619 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -1,9 +1,11 @@ """Code to wrap and make manipulating networkx graphs easier""" +from copy import deepcopy from typing import Any, Dict, Iterable, Mapping import networkx as nx +from .data_types.integers import make_integer_to_hold_ints from .representation import intermediate as ir from .tracing import BaseTracer from .tracing.tracing_helpers import create_graph_from_output_tracers @@ -57,3 +59,40 @@ class OPGraph: node_results[node] = node.evaluate({0: inputs[node.program_input_idx]}) return node_results + + def update_values_with_bounds(self, node_bounds: dict): + """Update nodes inputs and outputs values with data types able to hold data ranges measured + and passed in nodes_bounds + + Args: + node_bounds (dict): Dictionary with nodes as keys, holding dicts with a 'min' and 'max' + keys. Those bounds will be taken as the data range to be represented, per node. + """ + + node: ir.IntermediateNode + + for node in self.graph.nodes(): + current_node_bounds = node_bounds[node] + min_bound, max_bound = current_node_bounds["min"], current_node_bounds["max"] + + if not isinstance(node, ir.Input): + for output_value in node.outputs: + output_value.data_type = make_integer_to_hold_ints( + (min_bound, max_bound), force_signed=False + ) + else: + node.inputs[0].data_type = make_integer_to_hold_ints( + (min_bound, max_bound), force_signed=False + ) + node.outputs[0] = deepcopy(node.inputs[0]) + + # TODO: #57 manage multiple outputs from a node, probably requires an output_idx when + # adding an edge + assert len(node.outputs) == 1 + + successors = self.graph.succ[node] + for succ in successors: + edge_data = self.graph.get_edge_data(node, succ) + for edge in edge_data.values(): + input_idx = edge["input_idx"] + succ.inputs[input_idx] = deepcopy(node.outputs[0]) diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index 7c82bf6e7..398f190cf 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -9,77 +9,93 @@ from hdk.hnumpy.tracing import trace_numpy_function @pytest.mark.parametrize( - "function,input_ranges,expected_output_bounds", + "function,input_ranges,expected_output_bounds,expected_output_data_type", [ pytest.param( lambda x, y: x + y, ((-10, 10), (-10, 10)), (-20, 20), + Integer(6, is_signed=True), id="x + y, (-10, 10), (-10, 10), (-20, 20)", ), pytest.param( lambda x, y: x + y, ((-10, 2), (-4, 5)), (-14, 7), + Integer(5, is_signed=True), id="x + y, (-10, 2), (-4, 5), (-14, 9)", ), pytest.param( lambda x, y: x - y, ((-10, 10), (-10, 10)), (-20, 20), + Integer(6, is_signed=True), id="x - y, (-10, 10), (-10, 10), (-20, 20)", ), pytest.param( lambda x, y: x - y, ((-10, 2), (-4, 5)), (-15, 6), + Integer(5, is_signed=True), id="x - y, (-10, 2), (-4, 5), (-15, 6)", ), pytest.param( lambda x, y: x * y, ((-10, 10), (-10, 10)), (-100, 100), + Integer(8, is_signed=True), id="x * y, (-10, 10), (-10, 10), (-100, 100)", ), pytest.param( lambda x, y: x * y, ((-10, 2), (-4, 5)), (-50, 40), + Integer(7, is_signed=True), id="x * y, (-10, 2), (-4, 5), (-50, 40)", ), pytest.param( lambda x, y: x + x + y, ((-10, 10), (-10, 10)), (-30, 30), + Integer(6, is_signed=True), id="x + x + y, (-10, 10), (-10, 10), (-30, 30)", ), pytest.param( lambda x, y: x - x + y, ((-10, 10), (-10, 10)), (-10, 10), + Integer(5, is_signed=True), id="x - x + y, (-10, 10), (-10, 10), (-10, 10)", ), pytest.param( lambda x, y: x - x + y, ((-10, 2), (-4, 5)), (-4, 5), + Integer(4, is_signed=True), id="x - x + y, (-10, 2), (-4, 5), (-4, 5)", ), pytest.param( lambda x, y: x * y - x, ((-10, 10), (-10, 10)), (-110, 110), + Integer(8, is_signed=True), id="x * y - x, (-10, 10), (-10, 10), (-110, 110)", ), pytest.param( lambda x, y: x * y - x, ((-10, 2), (-4, 5)), (-40, 50), + Integer(7, is_signed=True), id="x * y - x, (-10, 2), (-4, 5), (-40, 50),", ), ], ) -def test_eval_op_graph_bounds_on_dataset(function, input_ranges, expected_output_bounds): +def test_eval_op_graph_bounds_on_dataset( + function, + input_ranges, + expected_output_bounds, + expected_output_data_type: Integer, +): """Test function for eval_op_graph_bounds_on_dataset""" op_graph = trace_numpy_function( @@ -99,3 +115,8 @@ def test_eval_op_graph_bounds_on_dataset(function, input_ranges, expected_output output_node_bounds = node_bounds[output_node] assert (output_node_bounds["min"], output_node_bounds["max"]) == expected_output_bounds + + assert EncryptedValue(Integer(64, True)) == output_node.outputs[0] + op_graph.update_values_with_bounds(node_bounds) + + assert expected_output_data_type == output_node.outputs[0].data_type From 0baa02549cae04b285bfe9c03bfd181d41ed59a6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 3 Aug 2021 14:42:05 +0200 Subject: [PATCH 0042/1104] build(mlir): setup things to use the MLIR/homomorphizer docker image --- .github/workflows/continuous-integration.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 4a35a7038..8976a685c 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -9,12 +9,20 @@ jobs: cancel-in-progress: true runs-on: ubuntu-20.04 + container: + image: ghcr.io/zama-ai/zamalang-compiler + credentials: + username: zama-bot + password: ${{ secrets.BOT_TOKEN }} strategy: matrix: python-version: [3.8] steps: - - uses: actions/checkout@v2 + - name: Install Git + run: apt-get install git -y + - name: Checkout Code + uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} From 6157e4680b99b445370b8211ff4eb1f230fd3566 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 3 Aug 2021 15:18:44 +0200 Subject: [PATCH 0043/1104] feat: adding constant management refs #49 --- hdk/common/data_types/scalars.py | 6 ++ hdk/common/debugging/draw_graph.py | 29 +++++--- hdk/common/operator_graph.py | 4 +- hdk/common/representation/intermediate.py | 28 +++++++- hdk/common/tracing/base_tracer.py | 59 +++++++++++++-- .../bounds_measurement/test_dataset_eval.py | 72 ++++++++++++++++++- .../representation/test_intermediate.py | 2 + tests/hnumpy/test_debugging.py | 26 ++++++- 8 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 hdk/common/data_types/scalars.py diff --git a/hdk/common/data_types/scalars.py b/hdk/common/data_types/scalars.py new file mode 100644 index 000000000..968d0b7fb --- /dev/null +++ b/hdk/common/data_types/scalars.py @@ -0,0 +1,6 @@ +"""File holding code to represent data types used for constants in programs""" + +from typing import Union + +# TODO: deal with more types +Scalars = Union[int, float] diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index e91623a04..d29c42f07 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -7,7 +7,13 @@ import networkx as nx from hdk.common.operator_graph import OPGraph from hdk.common.representation import intermediate as ir -IR_NODE_COLOR_MAPPING = {ir.Input: "blue", ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green"} +IR_NODE_COLOR_MAPPING = { + ir.Input: "blue", + ir.ConstantInput: "cyan", + ir.Add: "red", + ir.Sub: "yellow", + ir.Mul: "green", +} def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float = 1.0) -> Dict: @@ -117,10 +123,14 @@ def draw_graph( # For most types, we just pick the operation as the label, but for Input, # we take the name of the variable, ie the argument name of the function # to compile - label_dict = { - node: node.input_name if isinstance(node, ir.Input) else node.__class__.__name__ - for node in graph.nodes() - } + def get_proper_name(node): + if isinstance(node, ir.Input): + return node.input_name + if isinstance(node, ir.ConstantInput): + return str(node.constant_data) + return node.__class__.__name__ + + label_dict = {node: get_proper_name(node) for node in graph.nodes()} # Draw nodes nx.draw_networkx_nodes( @@ -222,7 +232,11 @@ def get_printable_graph(graph: Union[OPGraph, nx.MultiDiGraph]) -> str: for node in nx.topological_sort(graph): - if not isinstance(node, ir.Input): + if isinstance(node, ir.Input): + what_to_print = node.input_name + elif isinstance(node, ir.ConstantInput): + what_to_print = f"ConstantInput({node.constant_data})" + else: what_to_print = node.__class__.__name__ + "(" # Find all the names of the current predecessors of the node @@ -243,9 +257,6 @@ def get_printable_graph(graph: Union[OPGraph, nx.MultiDiGraph]) -> str: list_of_arg_name.sort() what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" - else: - what_to_print = node.input_name - returned_str += f"\n%{i} = {what_to_print}" map_table[node] = i i += 1 diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index b1ad61619..31927c1ad 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -27,11 +27,9 @@ class OPGraph: self.input_nodes = { node.program_input_idx: node for node in self.graph.nodes() - if len(self.graph.pred[node]) == 0 + if len(self.graph.pred[node]) == 0 and isinstance(node, ir.Input) } - assert all(map(lambda x: isinstance(x, ir.Input), self.input_nodes.values())) - graph_outputs = set(node for node in self.graph.nodes() if len(self.graph.succ[node]) == 0) assert set(self.output_nodes.values()) == graph_outputs diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 9bc0ffddb..163b87d60 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -6,6 +6,9 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple from ..data_types import BaseValue from ..data_types.dtypes_helpers import mix_values_determine_holding_dtype +from ..data_types.integers import Integer, get_bits_to_represent_int +from ..data_types.scalars import Scalars +from ..data_types.values import ClearValue class IntermediateNode(ABC): @@ -117,7 +120,7 @@ class Mul(IntermediateNode): class Input(IntermediateNode): - """Node representing an input of the numpy program""" + """Node representing an input of the program""" input_name: str program_input_idx: int @@ -136,3 +139,26 @@ class Input(IntermediateNode): def evaluate(self, inputs: Mapping[int, Any]) -> Any: return inputs[0] + + +class ConstantInput(IntermediateNode): + """Node representing a constant of the program""" + + constant_data: Scalars + + def __init__( + self, + constant_data: Scalars, + ) -> None: + super().__init__([]) + self.constant_data = constant_data + + # TODO: manage other cases, we can't call get_bits_to_represent_int + assert isinstance(constant_data, int) + is_signed = constant_data < 0 + self.outputs = [ + ClearValue(Integer(get_bits_to_represent_int(constant_data, is_signed), is_signed)) + ] + + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + return self.constant_data diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 73b674fef..3519f30d4 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -1,9 +1,10 @@ """This file holds the code that can be shared between tracers""" from abc import ABC -from typing import Any, Dict, List, Optional, Tuple, Type +from typing import Any, Dict, List, Optional, Tuple, Type, Union from ..data_types import BaseValue +from ..data_types.scalars import Scalars from ..representation import intermediate as ir @@ -26,7 +27,7 @@ class BaseTracer(ABC): def instantiate_output_tracers( self, - inputs: List["BaseTracer"], + inputs: List[Union["BaseTracer", Scalars]], computation_to_trace: Type[ir.IntermediateNode], op_args: Optional[Tuple[Any, ...]] = None, op_kwargs: Optional[Dict[str, Any]] = None, @@ -44,20 +45,30 @@ class BaseTracer(ABC): Returns: Tuple[BaseTracer, ...]: A tuple containing an BaseTracer per output function """ + + # For inputs which are actually constant, first convert into a tracer + def sanitize(inp): + if not isinstance(inp, BaseTracer): + return make_const_input_tracer(self.__class__, inp) + return inp + + sanitized_inputs = [sanitize(inp) for inp in inputs] + traced_computation = computation_to_trace( - map(lambda x: x.output, inputs), + map(lambda x: x.output, sanitized_inputs), op_args=op_args, op_kwargs=op_kwargs, ) output_tracers = tuple( - self.__class__(inputs, traced_computation, output_index) + self.__class__(sanitized_inputs, traced_computation, output_index) for output_index in range(len(traced_computation.outputs)) ) return output_tracers - def __add__(self, other: "BaseTracer") -> "BaseTracer": + def __add__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + result_tracer = self.instantiate_output_tracers( [self, other], ir.Add, @@ -66,7 +77,13 @@ class BaseTracer(ABC): assert len(result_tracer) == 1 return result_tracer[0] - def __sub__(self, other: "BaseTracer") -> "BaseTracer": + # With that is that x + 1 and 1 + x have the same graph. If we want to keep + # the order, we need to do as in __rsub__, ie mostly a copy of __sub__ + + # some changes + __radd__ = __add__ + + def __sub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + result_tracer = self.instantiate_output_tracers( [self, other], ir.Sub, @@ -75,7 +92,17 @@ class BaseTracer(ABC): assert len(result_tracer) == 1 return result_tracer[0] - def __mul__(self, other: "BaseTracer") -> "BaseTracer": + def __rsub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + + result_tracer = self.instantiate_output_tracers( + [other, self], + ir.Sub, + ) + + assert len(result_tracer) == 1 + return result_tracer[0] + + def __mul__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": result_tracer = self.instantiate_output_tracers( [self, other], ir.Mul, @@ -83,3 +110,21 @@ class BaseTracer(ABC): assert len(result_tracer) == 1 return result_tracer[0] + + # With that is that x * 3 and 3 * x have the same graph. If we want to keep + # the order, we need to do as in __rmul__, ie mostly a copy of __mul__ + + # some changes + __rmul__ = __mul__ + + +def make_const_input_tracer(tracer_class: Type[BaseTracer], constant_data: Scalars) -> BaseTracer: + """Helper function to create a tracer for a constant input + + Args: + tracer_class (Type[BaseTracer]): the class of tracer to create a ConstantInput for + constant_data (Scalars): the constant + + Returns: + BaseTracer: The BaseTracer for that constant + """ + return tracer_class([], ir.ConstantInput(constant_data), 0) diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index 398f190cf..2fd640c97 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -23,7 +23,28 @@ from hdk.hnumpy.tracing import trace_numpy_function ((-10, 2), (-4, 5)), (-14, 7), Integer(5, is_signed=True), - id="x + y, (-10, 2), (-4, 5), (-14, 9)", + id="x + y, (-10, 2), (-4, 5), (-14, 7)", + ), + pytest.param( + lambda x, y: x + y + 1, + ((-10, 2), (-4, 5)), + (-13, 8), + Integer(5, is_signed=True), + id="x + y + 1, (-10, 2), (-4, 5), (-13, 8)", + ), + pytest.param( + lambda x, y: x + y + (-3), + ((-10, 2), (-4, 5)), + (-17, 4), + Integer(6, is_signed=True), + id="x + y + 1, (-10, 2), (-4, 5), (-17, 4)", + ), + pytest.param( + lambda x, y: (1 + x) + y, + ((-10, 2), (-4, 5)), + (-13, 8), + Integer(5, is_signed=True), + id="(1 + x) + y, (-10, 2), (-4, 5), (-13, 8)", ), pytest.param( lambda x, y: x - y, @@ -39,6 +60,27 @@ from hdk.hnumpy.tracing import trace_numpy_function Integer(5, is_signed=True), id="x - y, (-10, 2), (-4, 5), (-15, 6)", ), + pytest.param( + lambda x, y: x - y - 42, + ((-10, 2), (-4, 5)), + (-57, -36), + Integer(7, is_signed=True), + id="x - y, (-10, 2), (-4, 5), (-57, -36)", + ), + pytest.param( + lambda x, y: 3 - x + y, + ((-10, 2), (-4, 5)), + (-3, 18), + Integer(6, is_signed=True), + id="x - y, (-10, 2), (-4, 5), (-3, 18)", + ), + pytest.param( + lambda x, y: (-13) - x + y, + ((-10, 2), (-4, 5)), + (-19, 2), + Integer(6, is_signed=True), + id="x - y, (-10, 2), (-4, 5), (-16, 2)", + ), pytest.param( lambda x, y: x * y, ((-10, 10), (-10, 10)), @@ -53,6 +95,27 @@ from hdk.hnumpy.tracing import trace_numpy_function Integer(7, is_signed=True), id="x * y, (-10, 2), (-4, 5), (-50, 40)", ), + pytest.param( + lambda x, y: (3 * x) * y, + ((-10, 2), (-4, 5)), + (-150, 120), + Integer(9, is_signed=True), + id="x * y, (-10, 2), (-4, 5), (-150, 120)", + ), + pytest.param( + lambda x, y: (x * 11) * y, + ((-10, 2), (-4, 5)), + (-550, 440), + Integer(11, is_signed=True), + id="x * y, (-10, 2), (-4, 5), (-550, 440)", + ), + pytest.param( + lambda x, y: (x * (-11)) * y, + ((-10, 2), (-4, 5)), + (-440, 550), + Integer(11, is_signed=True), + id="x * y, (-10, 2), (-4, 5), (-440, 550)", + ), pytest.param( lambda x, y: x + x + y, ((-10, 10), (-10, 10)), @@ -88,6 +151,13 @@ from hdk.hnumpy.tracing import trace_numpy_function Integer(7, is_signed=True), id="x * y - x, (-10, 2), (-4, 5), (-40, 50),", ), + pytest.param( + lambda x, y: (x * 3) * y - (x + 3) + (y - 13) + x * (11 + y) * (12 + y) + (15 - x), + ((-10, 2), (-4, 5)), + (-2846, 574), + Integer(13, is_signed=True), + id="x * y - x, (-10, 2), (-4, 5), (-2846, 574),", + ), ], ) def test_eval_op_graph_bounds_on_dataset( diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index ba8e8a629..2c4a665ec 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -29,6 +29,8 @@ from hdk.common.representation import intermediate as ir id="Mul", ), pytest.param(ir.Input(ClearValue(Integer(32, True)), "in", 0), [42], 42, id="Input"), + pytest.param(ir.ConstantInput(42), None, 42, id="ConstantInput"), + pytest.param(ir.ConstantInput(-42), None, -42, id="ConstantInput"), ], ) def test_evaluate( diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 5faf2fe6f..e28b956f0 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -13,11 +13,34 @@ from hdk.hnumpy import tracing [ (lambda x, y: x + y, "\n%0 = x\n%1 = y\n%2 = Add(0, 1)"), (lambda x, y: x - y, "\n%0 = x\n%1 = y\n%2 = Sub(0, 1)"), + (lambda x, y: x + x, "\n%0 = x\n%1 = Add(0, 0)"), ( lambda x, y: x + x - y * y * y + x, "\n%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)", ), + (lambda x, y: x + 1, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)"), + (lambda x, y: 1 + x, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)"), + (lambda x, y: (-1) + x, "\n%0 = x\n%1 = ConstantInput(-1)\n%2 = Add(0, 1)"), + (lambda x, y: 3 * x, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)"), + (lambda x, y: x * 3, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)"), + (lambda x, y: x * (-3), "\n%0 = x\n%1 = ConstantInput(-3)\n%2 = Mul(0, 1)"), + (lambda x, y: x - 11, "\n%0 = x\n%1 = ConstantInput(11)\n%2 = Sub(0, 1)"), + (lambda x, y: 11 - x, "\n%0 = ConstantInput(11)\n%1 = x\n%2 = Sub(0, 1)"), + (lambda x, y: (-11) - x, "\n%0 = ConstantInput(-11)\n%1 = x\n%2 = Sub(0, 1)"), + ( + lambda x, y: x + 13 - y * (-21) * y + 44, + "\n%0 = ConstantInput(44)" + "\n%1 = x" + "\n%2 = ConstantInput(13)" + "\n%3 = y" + "\n%4 = ConstantInput(-21)" + "\n%5 = Add(1, 2)" + "\n%6 = Mul(3, 4)" + "\n%7 = Mul(6, 3)" + "\n%8 = Sub(5, 7)" + "\n%9 = Add(8, 0)", + ), ], ) @pytest.mark.parametrize( @@ -48,6 +71,7 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): str_of_the_graph = get_printable_graph(graph) - print(f"\n{str_of_the_graph}\n") + print(f"\nGot {str_of_the_graph}\n") + print(f"\nExp {ref_graph_str}\n") assert str_of_the_graph == ref_graph_str From c6a2b4b35cc56ba0c7520e755bb6ffbe4982eda3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 3 Aug 2021 17:44:01 +0200 Subject: [PATCH 0044/1104] chore(reqs): update requirements --- poetry.lock | 224 +++++++++++++++++-------------------------------- pyproject.toml | 2 +- 2 files changed, 77 insertions(+), 149 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2337759c8..378649cef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,7 +16,7 @@ python-versions = "*" [[package]] name = "astroid" -version = "2.6.2" +version = "2.6.5" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -63,7 +63,7 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "21.6b0" +version = "21.7b0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -75,7 +75,7 @@ click = ">=7.1.2" mypy-extensions = ">=0.4.3" pathspec = ">=0.8.1,<1" regex = ">=2020.1.8" -toml = ">=0.10.1" +tomli = ">=0.2.6,<2.0.0" typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} @@ -103,7 +103,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.2" +version = "2.0.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -156,7 +156,7 @@ six = "*" [[package]] name = "diff-cover" -version = "6.2.0" +version = "6.2.1" description = "Automatically find diff lines that need test coverage." category = "dev" optional = false @@ -195,7 +195,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.1" +version = "4.6.3" description = "Read metadata from Python packages" category = "dev" optional = false @@ -232,7 +232,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.2" +version = "5.9.3" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -404,19 +404,14 @@ testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest [[package]] name = "networkx" -version = "2.6.1" +version = "2.6.2" description = "Python package for creating and manipulating graphs and networks" category = "main" optional = false python-versions = ">=3.7" -[package.dependencies] -matplotlib = ">=3.3" -numpy = ">=1.19" -pandas = ">=1.1" -scipy = ">=1.5,<1.6.1 || >1.6.1" - [package.extras] +default = ["numpy (>=1.19)", "scipy (>=1.5,!=1.6.1)", "matplotlib (>=3.3)", "pandas (>=1.1)"] developer = ["black (==21.5b1)", "pre-commit (>=2.12)"] doc = ["sphinx (>=4.0,<5.0)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx-gallery (>=0.9,<1.0)", "numpydoc (>=1.1)", "pillow (>=8.2)", "nb2plots (>=0.6)", "texext (>=0.6.6)"] extra = ["lxml (>=4.5)", "pygraphviz (>=1.7)", "pydot (>=1.4.1)"] @@ -424,7 +419,7 @@ test = ["pytest (>=6.2)", "pytest-cov (>=2.12)", "codecov (>=2.1)"] [[package]] name = "numpy" -version = "1.21.0" +version = "1.21.1" description = "NumPy is the fundamental package for array computing with Python." category = "main" optional = false @@ -441,29 +436,13 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" -[[package]] -name = "pandas" -version = "1.1.5" -description = "Powerful data structures for data analysis, time series, and statistics" -category = "main" -optional = false -python-versions = ">=3.6.1" - -[package.dependencies] -numpy = ">=1.15.4" -python-dateutil = ">=2.7.3" -pytz = ">=2017.2" - -[package.extras] -test = ["pytest (>=4.0.2)", "pytest-xdist", "hypothesis (>=3.58)"] - [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pillow" @@ -505,14 +484,14 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.9.3" +version = "2.9.6" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.6.2,<2.7" +astroid = ">=2.6.5,<2.7" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" @@ -617,17 +596,6 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] -[[package]] -name = "scipy" -version = "1.7.0" -description = "SciPy: Scientific Library for Python" -category = "main" -optional = false -python-versions = ">=3.7,<3.10" - -[package.dependencies] -numpy = ">=1.16.5,<1.23.0" - [[package]] name = "six" version = "1.16.0" @@ -646,7 +614,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.1.1" +version = "4.1.2" description = "Python documentation generator" category = "main" optional = false @@ -769,6 +737,14 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomli" +version = "1.2.0" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "typed-ast" version = "1.4.3" @@ -821,7 +797,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "e0f2368e94fc45bae93f9695b3702a12f4d2c013caaff22012e3abfc4f7af6ab" +content-hash = "11d441f0876a8ae3823fef994d552c7982d0122568acbfad3d6a0425b10260f0" [metadata.files] alabaster = [ @@ -833,8 +809,8 @@ appdirs = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] astroid = [ - {file = "astroid-2.6.2-py3-none-any.whl", hash = "sha256:606b2911d10c3dcf35e58d2ee5c97360e8477d7b9f3efc3f24811c93e6fc2cd9"}, - {file = "astroid-2.6.2.tar.gz", hash = "sha256:38b95085e9d92e2ca06cf8b35c12a74fa81da395a6f9e65803742e6509c05892"}, + {file = "astroid-2.6.5-py3-none-any.whl", hash = "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925"}, + {file = "astroid-2.6.5.tar.gz", hash = "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -849,8 +825,8 @@ babel = [ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] black = [ - {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, - {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, + {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, + {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, @@ -861,8 +837,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.2.tar.gz", hash = "sha256:951567c2f7433a70ab63f1be67e5ee05d3925d9423306ecb71a3b272757bcc95"}, - {file = "charset_normalizer-2.0.2-py3-none-any.whl", hash = "sha256:3c502a35807c9df35697b0f44b1d65008f83071ff29c69677c7c22573bc5a45a"}, + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, ] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, @@ -931,8 +907,8 @@ cycler = [ {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] diff-cover = [ - {file = "diff_cover-6.2.0-py3-none-any.whl", hash = "sha256:c2d5c6f6ec8dceddabc4abcefc984e937cd7e5f14787968c991c57f1e2b13c03"}, - {file = "diff_cover-6.2.0.tar.gz", hash = "sha256:66a20cb0f0a631792849af5f1b5e2dff254937c24983a4b63b8d483e0e9cf7a6"}, + {file = "diff_cover-6.2.1-py3-none-any.whl", hash = "sha256:cfe6333c9b83a28f91214e06e3a86108aeeb6a9a31233944ce8ef236121ba899"}, + {file = "diff_cover-6.2.1.tar.gz", hash = "sha256:b22fe97d118fadb2528bbc0a1f9fc370491b29b1b3fe2e2f1cdb94bf028814c7"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -947,8 +923,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, - {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, + {file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"}, + {file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"}, ] inflect = [ {file = "inflect-5.3.0-py3-none-any.whl", hash = "sha256:42560be16af702a21d43d59427f276b5aed79efb1ded9b713468c081f4353d10"}, @@ -959,8 +935,8 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.2-py3-none-any.whl", hash = "sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813"}, - {file = "isort-5.9.2.tar.gz", hash = "sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"}, + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, @@ -1131,79 +1107,48 @@ myst-parser = [ {file = "myst_parser-0.15.1-py3-none-any.whl", hash = "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"}, ] networkx = [ - {file = "networkx-2.6.1-py3-none-any.whl", hash = "sha256:aa21cd7c7e0696672885866b2736a32bfd4bd20996ab99fc6142edf9357d3241"}, - {file = "networkx-2.6.1.tar.gz", hash = "sha256:bf4cb807d1bccf1593c7d0742d9127d9e04e021867299082658b0fc3907924e8"}, + {file = "networkx-2.6.2-py3-none-any.whl", hash = "sha256:5fcb7004be69e8fbdf07dcb502efa5c77cadcaad6982164134eeb9721f826c2e"}, + {file = "networkx-2.6.2.tar.gz", hash = "sha256:2306f1950ce772c5a59a57f5486d59bb9cab98497c45fc49cbc45ac0dec119bb"}, ] numpy = [ - {file = "numpy-1.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d5caa946a9f55511e76446e170bdad1d12d6b54e17a2afe7b189112ed4412bb8"}, - {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ac4fd578322842dbda8d968e3962e9f22e862b6ec6e3378e7415625915e2da4d"}, - {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:598fe100b2948465cf3ed64b1a326424b5e4be2670552066e17dfaa67246011d"}, - {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c55407f739f0bfcec67d0df49103f9333edc870061358ac8a8c9e37ea02fcd2"}, - {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75579acbadbf74e3afd1153da6177f846212ea2a0cc77de53523ae02c9256513"}, - {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc367c86eb87e5b7c9592935620f22d13b090c609f1b27e49600cd033b529f54"}, - {file = "numpy-1.21.0-cp37-cp37m-win32.whl", hash = "sha256:d89b0dc7f005090e32bb4f9bf796e1dcca6b52243caf1803fdd2b748d8561f63"}, - {file = "numpy-1.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eda2829af498946c59d8585a9fd74da3f810866e05f8df03a86f70079c7531dd"}, - {file = "numpy-1.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1a784e8ff7ea2a32e393cc53eb0003eca1597c7ca628227e34ce34eb11645a0e"}, - {file = "numpy-1.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bba474a87496d96e61461f7306fba2ebba127bed7836212c360f144d1e72ac54"}, - {file = "numpy-1.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd0a359c1c17f00cb37de2969984a74320970e0ceef4808c32e00773b06649d9"}, - {file = "numpy-1.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4d5a86a5257843a18fb1220c5f1c199532bc5d24e849ed4b0289fb59fbd4d8f"}, - {file = "numpy-1.21.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:620732f42259eb2c4642761bd324462a01cdd13dd111740ce3d344992dd8492f"}, - {file = "numpy-1.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9205711e5440954f861ceeea8f1b415d7dd15214add2e878b4d1cf2bcb1a914"}, - {file = "numpy-1.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ad09f55cc95ed8d80d8ab2052f78cc21cb231764de73e229140d81ff49d8145e"}, - {file = "numpy-1.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1f2fb2da242568af0271455b89aee0f71e4e032086ee2b4c5098945d0e11cf6"}, - {file = "numpy-1.21.0-cp38-cp38-win32.whl", hash = "sha256:e58ddb53a7b4959932f5582ac455ff90dcb05fac3f8dcc8079498d43afbbde6c"}, - {file = "numpy-1.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:d2910d0a075caed95de1a605df00ee03b599de5419d0b95d55342e9a33ad1fb3"}, - {file = "numpy-1.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a290989cd671cd0605e9c91a70e6df660f73ae87484218e8285c6522d29f6e38"}, - {file = "numpy-1.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3537b967b350ad17633b35c2f4b1a1bbd258c018910b518c30b48c8e41272717"}, - {file = "numpy-1.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc6c650f8700ce1e3a77668bb7c43e45c20ac06ae00d22bdf6760b38958c883"}, - {file = "numpy-1.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:709884863def34d72b183d074d8ba5cfe042bc3ff8898f1ffad0209161caaa99"}, - {file = "numpy-1.21.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bebab3eaf0641bba26039fb0b2c5bf9b99407924b53b1ea86e03c32c64ef5aef"}, - {file = "numpy-1.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf680682ad0a3bef56dae200dbcbac2d57294a73e5b0f9864955e7dd7c2c2491"}, - {file = "numpy-1.21.0-cp39-cp39-win32.whl", hash = "sha256:d95d16204cd51ff1a1c8d5f9958ce90ae190be81d348b514f9be39f878b8044a"}, - {file = "numpy-1.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:2ba579dde0563f47021dcd652253103d6fd66165b18011dce1a0609215b2791e"}, - {file = "numpy-1.21.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c40e6b860220ed862e8097b8f81c9af6d7405b723f4a7af24a267b46f90e461"}, - {file = "numpy-1.21.0.zip", hash = "sha256:e80fe25cba41c124d04c662f33f6364909b985f2eb5998aaa5ae4b9587242cce"}, + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, ] packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] -pandas = [ - {file = "pandas-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d"}, - {file = "pandas-1.1.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5a780260afc88268a9d3ac3511d8f494fdcf637eece62fb9eb656a63d53eb7ca"}, - {file = "pandas-1.1.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b61080750d19a0122469ab59b087380721d6b72a4e7d962e4d7e63e0c4504814"}, - {file = "pandas-1.1.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:0de3ddb414d30798cbf56e642d82cac30a80223ad6fe484d66c0ce01a84d6f2f"}, - {file = "pandas-1.1.5-cp36-cp36m-win32.whl", hash = "sha256:70865f96bb38fec46f7ebd66d4b5cfd0aa6b842073f298d621385ae3898d28b5"}, - {file = "pandas-1.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:19a2148a1d02791352e9fa637899a78e371a3516ac6da5c4edc718f60cbae648"}, - {file = "pandas-1.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26fa92d3ac743a149a31b21d6f4337b0594b6302ea5575b37af9ca9611e8981a"}, - {file = "pandas-1.1.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c16d59c15d946111d2716856dd5479221c9e4f2f5c7bc2d617f39d870031e086"}, - {file = "pandas-1.1.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3be7a7a0ca71a2640e81d9276f526bca63505850add10206d0da2e8a0a325dae"}, - {file = "pandas-1.1.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:573fba5b05bf2c69271a32e52399c8de599e4a15ab7cec47d3b9c904125ab788"}, - {file = "pandas-1.1.5-cp37-cp37m-win32.whl", hash = "sha256:21b5a2b033380adbdd36b3116faaf9a4663e375325831dac1b519a44f9e439bb"}, - {file = "pandas-1.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:24c7f8d4aee71bfa6401faeba367dd654f696a77151a8a28bc2013f7ced4af98"}, - {file = "pandas-1.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2860a97cbb25444ffc0088b457da0a79dc79f9c601238a3e0644312fcc14bf11"}, - {file = "pandas-1.1.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5008374ebb990dad9ed48b0f5d0038124c73748f5384cc8c46904dace27082d9"}, - {file = "pandas-1.1.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2c2f7c670ea4e60318e4b7e474d56447cf0c7d83b3c2a5405a0dbb2600b9c48e"}, - {file = "pandas-1.1.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0a643bae4283a37732ddfcecab3f62dd082996021b980f580903f4e8e01b3c5b"}, - {file = "pandas-1.1.5-cp38-cp38-win32.whl", hash = "sha256:5447ea7af4005b0daf695a316a423b96374c9c73ffbd4533209c5ddc369e644b"}, - {file = "pandas-1.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:4c62e94d5d49db116bef1bd5c2486723a292d79409fc9abd51adf9e05329101d"}, - {file = "pandas-1.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:731568be71fba1e13cae212c362f3d2ca8932e83cb1b85e3f1b4dd77d019254a"}, - {file = "pandas-1.1.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c61c043aafb69329d0f961b19faa30b1dab709dd34c9388143fc55680059e55a"}, - {file = "pandas-1.1.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2b1c6cd28a0dfda75c7b5957363333f01d370936e4c6276b7b8e696dd500582a"}, - {file = "pandas-1.1.5-cp39-cp39-win32.whl", hash = "sha256:c94ff2780a1fd89f190390130d6d36173ca59fcfb3fe0ff596f9a56518191ccb"}, - {file = "pandas-1.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:edda9bacc3843dfbeebaf7a701763e68e741b08fccb889c003b0a52f0ee95782"}, - {file = "pandas-1.1.5.tar.gz", hash = "sha256:f10fc41ee3c75a474d3bdf68d396f10782d013d7f67db99c0efbfd0acb99701b"}, -] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pillow = [ - {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, - {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, - {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, - {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, - {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, @@ -1252,8 +1197,8 @@ pygments = [ {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] pylint = [ - {file = "pylint-2.9.3-py3-none-any.whl", hash = "sha256:5d46330e6b8886c31b5e3aba5ff48c10f4aa5e76cbf9002c6544306221e63fbc"}, - {file = "pylint-2.9.3.tar.gz", hash = "sha256:23a1dc8b30459d78e9ff25942c61bb936108ccbe29dd9e71c01dc8274961709a"}, + {file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"}, + {file = "pylint-2.9.6.tar.gz", hash = "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -1353,27 +1298,6 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] -scipy = [ - {file = "scipy-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:821e75f5c16cd7b0ab0ffe7eb9917e5af7b48c25306b4777287de8d792a5f7f3"}, - {file = "scipy-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e7df79b42c3015058a5554bfeab6fd4c9906c46560c9ddebb5c652840f3e182"}, - {file = "scipy-1.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0572256c10ddd058e3d315c555538671ddb2737f27eb56189bfbc3483391403f"}, - {file = "scipy-1.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b77ee5e3a9507622e7f98b16122242a3903397f98d1fe3bc269d904a9025e2bc"}, - {file = "scipy-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:53116abd5060a5b4a58489cf689bee259b779e6b7ecd4ce366e7147aa7c9626e"}, - {file = "scipy-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e7b733d4d98e604109715e11f2ab9340eb45d53f803634ed730039070fc3bc11"}, - {file = "scipy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4ef3d4df8af40cb6f4d4eaf7b02780109ebabeec334cda26a7899ec9d8de9176"}, - {file = "scipy-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd4399d4388ca0239a4825e312b3e61b60f743dd6daf49e5870837716502a92a"}, - {file = "scipy-1.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80df8af7039bce92fb4cd1ceb056258631b11b3c627384e2d29bb48d44c0cae7"}, - {file = "scipy-1.7.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6130e22bf6ee506f7cddde7e0515296d97eb6c6c94f7ef5103c2b77aec5833a7"}, - {file = "scipy-1.7.0-cp38-cp38-win32.whl", hash = "sha256:97ca4552ace1c313707058e774609af59644321e278c3a539322fab2fb09b943"}, - {file = "scipy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:c5d012cb82cc1dcfa72609abaabb4a4ed8113e3e8ac43464508a418c146be57d"}, - {file = "scipy-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5eb8f054eebb351af7490bbb57465ba9662c4e16e1786655c6c7ed530eb9a74e"}, - {file = "scipy-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4b89c223bd09460b52b669e2e642cab73c28855b540e6ed029692546a86f8d"}, - {file = "scipy-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e685fdbfa5b989af4338b29c408b9157ea6addec15d661104c437980c292be5"}, - {file = "scipy-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3595c8b64970c9e5a3f137fa1a9eb64da417e78fb7991d0b098b18a00b776d88"}, - {file = "scipy-1.7.0-cp39-cp39-win32.whl", hash = "sha256:5a983d3cebc27294897951a494cebd78af2eae37facf75d9e4ad4f1f62229860"}, - {file = "scipy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:aef6e922aea6f2e6bbb539b413c85210a9ee32757535b84204ebd22723e69704"}, - {file = "scipy-1.7.0.tar.gz", hash = "sha256:998c5e6ea649489302de2c0bc026ed34284f531df89d2bdc8df3a0d44d165739"}, -] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1383,8 +1307,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sphinx = [ - {file = "Sphinx-4.1.1-py3-none-any.whl", hash = "sha256:3d513088236eef51e5b0adb78b0492eb22cc3b8ccdb0b36dd021173b365d4454"}, - {file = "Sphinx-4.1.1.tar.gz", hash = "sha256:23c846a1841af998cb736218539bb86d16f5eb95f5760b1966abcd2d584e62b8"}, + {file = "Sphinx-4.1.2-py3-none-any.whl", hash = "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"}, + {file = "Sphinx-4.1.2.tar.gz", hash = "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, @@ -1418,6 +1342,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomli = [ + {file = "tomli-1.2.0-py3-none-any.whl", hash = "sha256:056f0376bf5a6b182c513f9582c1e5b0487265eb6c48842b69aa9ca1cd5f640a"}, + {file = "tomli-1.2.0.tar.gz", hash = "sha256:d60e681734099207a6add7a10326bc2ddd1fdc36c1b0f547d00ef73ac63739c2"}, +] typed-ast = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, diff --git a/pyproject.toml b/pyproject.toml index 0b1603c32..fc7ea3f50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ matplotlib = "^3.4.2" [tool.poetry.dev-dependencies] isort = "^5.9.2" -black = "21.6b0" +black = "21.7b0" pylint = "^2.9.3" pytest = "^6.2.4" pytest-cov = "^2.12.1" From 73f21c79a6be3362ad62af0117b3efe5f84fe65f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 2 Aug 2021 13:26:13 +0200 Subject: [PATCH 0045/1104] dev: add a function to check that a program is an actual integer program --- hdk/common/__init__.py | 1 + hdk/common/common_helpers.py | 51 +++++++++++++++++++++++++ tests/common/test_common_helpers.py | 58 +++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 hdk/common/common_helpers.py create mode 100644 tests/common/test_common_helpers.py diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index eecdb8723..f498f0d8d 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,2 +1,3 @@ """Module for shared data structures and code""" from . import data_types, debugging, representation +from .common_helpers import check_op_graph_is_integer_program diff --git a/hdk/common/common_helpers.py b/hdk/common/common_helpers.py new file mode 100644 index 000000000..e0c872c9a --- /dev/null +++ b/hdk/common/common_helpers.py @@ -0,0 +1,51 @@ +"""File to hold some helper code""" + +from typing import List, Optional + +from .data_types.integers import Integer +from .operator_graph import OPGraph +from .representation import intermediate as ir + + +def ir_nodes_has_integer_input_and_output(node: ir.IntermediateNode) -> bool: + """Check if an ir node has Integer inputs and outputs + + Args: + node (ir.IntermediateNode): Node to check + + Returns: + bool: True if all input and output values hold Integers + """ + return all(map(lambda x: isinstance(x.data_type, Integer), node.inputs)) and all( + map(lambda x: isinstance(x.data_type, Integer), node.outputs) + ) + + +# This check makes sense as long as the compiler backend only manages integers, to be removed in the +# long run probably +def check_op_graph_is_integer_program( + op_graph: OPGraph, + offending_nodes_out: Optional[List[ir.IntermediateNode]] = None, +) -> bool: + """Check if an op_graph inputs, outputs and intermediate values are Integers + + Args: + op_graph (OPGraph): The OPGraph to check + offending_nodes_out (Optional[List[ir.IntermediateNode]]): Optionally pass a list that will + be populated with offending nodes, the list will be cleared before being filled + + Returns: + bool: True if inputs, outputs and intermediate values are Integers, False otherwise + """ + offending_nodes = [] if offending_nodes_out is None else offending_nodes_out + + assert isinstance( + offending_nodes, list + ), f"offending_nodes_out must be a list, got {type(offending_nodes_out)}" + + offending_nodes.clear() + offending_nodes.extend( + node for node in op_graph.graph.nodes() if not ir_nodes_has_integer_input_and_output(node) + ) + + return len(offending_nodes) == 0 diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py new file mode 100644 index 000000000..fdbee8538 --- /dev/null +++ b/tests/common/test_common_helpers.py @@ -0,0 +1,58 @@ +"""Test file for common helpers""" + +from copy import deepcopy + +from hdk.common import check_op_graph_is_integer_program +from hdk.common.data_types.base import BaseDataType +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.hnumpy.tracing import trace_numpy_function + + +class DummyNotInteger(BaseDataType): + """Dummy helper data type class""" + + +def test_check_op_graph_is_integer_program(): + """Test function for check_op_graph_is_integer_program""" + + def function(x, y): + return x + y - y * y + x * y + + op_graph = trace_numpy_function( + function, {"x": EncryptedValue(Integer(64, True)), "y": EncryptedValue(Integer(64, True))} + ) + + # Test without and with output list + offending_nodes = [] + assert check_op_graph_is_integer_program(op_graph) + assert check_op_graph_is_integer_program(op_graph, offending_nodes) + assert len(offending_nodes) == 0 + + op_graph_copy = deepcopy(op_graph) + op_graph_copy.output_nodes[0].outputs[0].data_type = DummyNotInteger() + + offending_nodes = [] + assert not check_op_graph_is_integer_program(op_graph_copy) + assert not check_op_graph_is_integer_program(op_graph_copy, offending_nodes) + assert len(offending_nodes) == 1 + assert offending_nodes == [op_graph_copy.output_nodes[0]] + + op_graph_copy = deepcopy(op_graph) + op_graph_copy.input_nodes[0].inputs[0].data_type = DummyNotInteger() + + offending_nodes = [] + assert not check_op_graph_is_integer_program(op_graph_copy) + assert not check_op_graph_is_integer_program(op_graph_copy, offending_nodes) + assert len(offending_nodes) == 1 + assert offending_nodes == [op_graph_copy.input_nodes[0]] + + op_graph_copy = deepcopy(op_graph) + op_graph_copy.input_nodes[0].inputs[0].data_type = DummyNotInteger() + op_graph_copy.input_nodes[1].inputs[0].data_type = DummyNotInteger() + + offending_nodes = [] + assert not check_op_graph_is_integer_program(op_graph_copy) + assert not check_op_graph_is_integer_program(op_graph_copy, offending_nodes) + assert len(offending_nodes) == 2 + assert set(offending_nodes) == set([op_graph_copy.input_nodes[0], op_graph_copy.input_nodes[1]]) From 6a80b065fc46161d9c946df1e11a65c3b90123f6 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 4 Aug 2021 15:49:36 +0200 Subject: [PATCH 0046/1104] test: testing eval_op_graph_bounds_on_dataset with multiple outputs refs #74 --- .../bounds_measurement/test_dataset_eval.py | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index 2fd640c97..f2e331412 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -1,5 +1,7 @@ """Test file for bounds evaluation with a dataset""" +from typing import Tuple + import pytest from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset @@ -168,6 +170,39 @@ def test_eval_op_graph_bounds_on_dataset( ): """Test function for eval_op_graph_bounds_on_dataset""" + test_eval_op_graph_bounds_on_dataset_multiple_output( + function, + input_ranges, + (expected_output_bounds,), + (expected_output_data_type,), + ) + + +@pytest.mark.parametrize( + "function,input_ranges,expected_output_bounds,expected_output_data_type", + [ + pytest.param( + lambda x, y: (x + 1, y + 10), + ((-1, 1), (3, 4)), + ((0, 2), (13, 14)), + (Integer(2, is_signed=False), Integer(4, is_signed=False)), + ), + pytest.param( + lambda x, y: (x + y + 1, x * y + 42), + ((-1, 1), (3, 4)), + ((3, 6), (38, 46)), + (Integer(3, is_signed=False), Integer(6, is_signed=False)), + ), + ], +) +def test_eval_op_graph_bounds_on_dataset_multiple_output( + function, + input_ranges, + expected_output_bounds, + expected_output_data_type: Tuple[Integer], +): + """Test function for eval_op_graph_bounds_on_dataset""" + op_graph = trace_numpy_function( function, {"x": EncryptedValue(Integer(64, True)), "y": EncryptedValue(Integer(64, True))} ) @@ -181,12 +216,14 @@ def test_eval_op_graph_bounds_on_dataset( op_graph, data_gen(*tuple(map(lambda x: range(x[0], x[1] + 1), input_ranges))) ) - output_node = op_graph.output_nodes[0] - output_node_bounds = node_bounds[output_node] + for i, output_node in op_graph.output_nodes.items(): + output_node_bounds = node_bounds[output_node] - assert (output_node_bounds["min"], output_node_bounds["max"]) == expected_output_bounds + assert (output_node_bounds["min"], output_node_bounds["max"]) == expected_output_bounds[i] + + assert EncryptedValue(Integer(64, True)) == output_node.outputs[0] - assert EncryptedValue(Integer(64, True)) == output_node.outputs[0] op_graph.update_values_with_bounds(node_bounds) - assert expected_output_data_type == output_node.outputs[0].data_type + for i, output_node in op_graph.output_nodes.items(): + assert expected_output_data_type[i] == output_node.outputs[0].data_type From 078c8dc8f1fd3a8361114d45b7f222fc1f0c4b87 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 4 Aug 2021 17:23:26 +0200 Subject: [PATCH 0047/1104] fix: conform to more reasonable style to check type of object (#78) --- hdk/common/data_types/dtypes_helpers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 85f6ad2f7..a7036b950 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -6,7 +6,7 @@ from .base import BaseDataType from .integers import Integer from .values import BaseValue, ClearValue, EncryptedValue -INTEGER_TYPES = set([Integer]) +INTEGER_TYPES = (Integer,) def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: @@ -18,9 +18,8 @@ def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is an encrypted value of type Integer """ - return ( - isinstance(value_to_check, EncryptedValue) - and type(value_to_check.data_type) in INTEGER_TYPES + return isinstance(value_to_check, EncryptedValue) and isinstance( + value_to_check.data_type, INTEGER_TYPES ) From 1ee8195af0213519b4612420cde7b8445a205ebc Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 4 Aug 2021 16:56:13 +0200 Subject: [PATCH 0048/1104] feat: supporting several outputs in print and draw of graphs refs #76 --- hdk/common/debugging/draw_graph.py | 31 +++++++++++++------- tests/hnumpy/test_debugging.py | 45 ++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index d29c42f07..9d404c94c 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -1,5 +1,5 @@ """functions to draw the different graphs we can generate in the package, eg to debug""" -from typing import Any, Dict, List, Union +from typing import Any, Dict, List import matplotlib.pyplot as plt import networkx as nx @@ -13,6 +13,7 @@ IR_NODE_COLOR_MAPPING = { ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green", + "output": "magenta", } @@ -87,7 +88,7 @@ def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float def draw_graph( - graph: Union[OPGraph, nx.MultiDiGraph], + opgraph: OPGraph, block_until_user_closes_graph: bool = True, draw_edge_numbers: bool = True, ) -> None: @@ -95,7 +96,7 @@ def draw_graph( Draw a graph Args: - graph (Union[OPGraph, nx.MultiDiGraph]): The graph that we want to draw + graph (OPGraph): The graph that we want to draw block_until_user_closes_graph (bool): if True, will wait the user to close the figure before continuing; False is useful for the CI tests draw_edge_numbers (bool): if True, add the edge number on the arrow @@ -111,14 +112,20 @@ def draw_graph( # FIXME: less variables # pylint: disable=too-many-locals - # Allow to pass either OPGraph or an nx graph, manage this here - graph = graph.graph if isinstance(graph, OPGraph) else graph + assert isinstance(opgraph, OPGraph) + set_of_nodes_which_are_outputs = set(opgraph.output_nodes.values()) + graph = opgraph.graph # Positions of the node pos = human_readable_layout(graph) # Colors and labels - color_map = [IR_NODE_COLOR_MAPPING[type(node)] for node in graph.nodes()] + def get_color(node): + if node in set_of_nodes_which_are_outputs: + return IR_NODE_COLOR_MAPPING["output"] + return IR_NODE_COLOR_MAPPING[type(node)] + + color_map = [get_color(node) for node in graph.nodes()] # For most types, we just pick the operation as the label, but for Input, # we take the name of the variable, ie the argument name of the function @@ -212,18 +219,19 @@ def draw_graph( # pylint: enable=too-many-locals -def get_printable_graph(graph: Union[OPGraph, nx.MultiDiGraph]) -> str: +def get_printable_graph(opgraph: OPGraph) -> str: """Return a string representing a graph Args: - graph (Union[OPGraph, nx.MultiDiGraph]): The graph that we want to draw + graph (OPGraph): The graph that we want to draw Returns: str: a string to print or save in a file """ - # Allow to pass either OPGraph or an nx graph, manage this here - graph = graph.graph if isinstance(graph, OPGraph) else graph + assert isinstance(opgraph, OPGraph) + list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) + graph = opgraph.graph returned_str = "" @@ -261,4 +269,7 @@ def get_printable_graph(graph: Union[OPGraph, nx.MultiDiGraph]) -> str: map_table[node] = i i += 1 + return_part = ", ".join(["%" + str(map_table[n]) for n in list_of_nodes_which_are_outputs]) + returned_str += f"\nreturn({return_part})" + return returned_str diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index e28b956f0..9187a91a1 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -11,23 +11,23 @@ from hdk.hnumpy import tracing @pytest.mark.parametrize( "lambda_f,ref_graph_str", [ - (lambda x, y: x + y, "\n%0 = x\n%1 = y\n%2 = Add(0, 1)"), - (lambda x, y: x - y, "\n%0 = x\n%1 = y\n%2 = Sub(0, 1)"), - (lambda x, y: x + x, "\n%0 = x\n%1 = Add(0, 0)"), + (lambda x, y: x + y, "\n%0 = x\n%1 = y\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: x - y, "\n%0 = x\n%1 = y\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: x + x, "\n%0 = x\n%1 = Add(0, 0)\nreturn(%1)"), ( lambda x, y: x + x - y * y * y + x, "\n%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" - "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)", + "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)\nreturn(%6)", ), - (lambda x, y: x + 1, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)"), - (lambda x, y: 1 + x, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)"), - (lambda x, y: (-1) + x, "\n%0 = x\n%1 = ConstantInput(-1)\n%2 = Add(0, 1)"), - (lambda x, y: 3 * x, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)"), - (lambda x, y: x * 3, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)"), - (lambda x, y: x * (-3), "\n%0 = x\n%1 = ConstantInput(-3)\n%2 = Mul(0, 1)"), - (lambda x, y: x - 11, "\n%0 = x\n%1 = ConstantInput(11)\n%2 = Sub(0, 1)"), - (lambda x, y: 11 - x, "\n%0 = ConstantInput(11)\n%1 = x\n%2 = Sub(0, 1)"), - (lambda x, y: (-11) - x, "\n%0 = ConstantInput(-11)\n%1 = x\n%2 = Sub(0, 1)"), + (lambda x, y: x + 1, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: 1 + x, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: (-1) + x, "\n%0 = x\n%1 = ConstantInput(-1)\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: 3 * x, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)\nreturn(%2)"), + (lambda x, y: x * 3, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)\nreturn(%2)"), + (lambda x, y: x * (-3), "\n%0 = x\n%1 = ConstantInput(-3)\n%2 = Mul(0, 1)\nreturn(%2)"), + (lambda x, y: x - 11, "\n%0 = x\n%1 = ConstantInput(11)\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: 11 - x, "\n%0 = ConstantInput(11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: (-11) - x, "\n%0 = ConstantInput(-11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), ( lambda x, y: x + 13 - y * (-21) * y + 44, "\n%0 = ConstantInput(44)" @@ -39,7 +39,24 @@ from hdk.hnumpy import tracing "\n%6 = Mul(3, 4)" "\n%7 = Mul(6, 3)" "\n%8 = Sub(5, 7)" - "\n%9 = Add(8, 0)", + "\n%9 = Add(8, 0)" + "\nreturn(%9)", + ), + # Multiple outputs + ( + lambda x, y: (x + 1, x + y + 2), + "\n%0 = x" + "\n%1 = ConstantInput(1)" + "\n%2 = ConstantInput(2)" + "\n%3 = y" + "\n%4 = Add(0, 1)" + "\n%5 = Add(0, 3)" + "\n%6 = Add(5, 2)" + "\nreturn(%4, %6)", + ), + ( + lambda x, y: (y, x), + "\n%0 = y\n%1 = x\nreturn(%0, %1)", ), ], ) From 1771bc6e52bf350825fe9ee942e616032c404861 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 4 Aug 2021 18:13:15 +0200 Subject: [PATCH 0049/1104] fix: fix #80 closes #80 --- hdk/common/operator_graph.py | 4 ---- tests/hnumpy/test_debugging.py | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index 31927c1ad..4a93182c1 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -30,10 +30,6 @@ class OPGraph: if len(self.graph.pred[node]) == 0 and isinstance(node, ir.Input) } - graph_outputs = set(node for node in self.graph.nodes() if len(self.graph.succ[node]) == 0) - - assert set(self.output_nodes.values()) == graph_outputs - def evaluate(self, inputs: Mapping[int, Any]) -> Dict[ir.IntermediateNode, Any]: """Function to evaluate a graph and get intermediate values for all nodes diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 9187a91a1..ea611a791 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -58,6 +58,10 @@ from hdk.hnumpy import tracing lambda x, y: (y, x), "\n%0 = y\n%1 = x\nreturn(%0, %1)", ), + ( + lambda x, y: (x, x + 1), + "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%0, %2)", + ), ], ) @pytest.mark.parametrize( From 0c275c5f43b06347f2548531dbac3a450e60cde5 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 4 Aug 2021 19:23:34 +0200 Subject: [PATCH 0050/1104] dev(floats): add Float class to represent a floating point value --- hdk/common/data_types/floats.py | 24 +++++++++++++ tests/common/data_types/test_floats.py | 50 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 hdk/common/data_types/floats.py create mode 100644 tests/common/data_types/test_floats.py diff --git a/hdk/common/data_types/floats.py b/hdk/common/data_types/floats.py new file mode 100644 index 000000000..7021886e0 --- /dev/null +++ b/hdk/common/data_types/floats.py @@ -0,0 +1,24 @@ +"""This file holds the definitions for floating point types""" + +from . import base + + +class Float(base.BaseDataType): + """Class representing a float""" + + # bit_width is the total number of bits used to represent a floating point number, including + # sign bit, exponent and mantissa + bit_width: int + + def __init__(self, bit_width: int) -> None: + self.bit_width = bit_width + + def __repr__(self) -> str: + return f"{self.__class__.__name__}<{self.bit_width} bits>" + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.bit_width == other.bit_width + + +Float32 = lambda: Float(32) +Float64 = lambda: Float(64) diff --git a/tests/common/data_types/test_floats.py b/tests/common/data_types/test_floats.py new file mode 100644 index 000000000..49a7ba380 --- /dev/null +++ b/tests/common/data_types/test_floats.py @@ -0,0 +1,50 @@ +"""Test file for float data types""" + + +import pytest + +from hdk.common.data_types.floats import Float, Float32, Float64 + + +@pytest.mark.parametrize( + "float_,expected_repr_str", + [ + pytest.param( + Float32(), + "Float<32 bits>", + id="Float32", + ), + pytest.param( + Float(32), + "Float<32 bits>", + id="32 bits Float", + ), + pytest.param( + Float64(), + "Float<64 bits>", + id="Float64", + ), + pytest.param( + Float(64), + "Float<64 bits>", + id="64 bits Float", + ), + ], +) +def test_floats_repr(float_: Float, expected_repr_str: str): + """Test float repr""" + assert float_.__repr__() == expected_repr_str + + +@pytest.mark.parametrize( + "float_1,float_2,expected_equal", + [ + pytest.param(Float32(), Float(32), True), + pytest.param(Float(64), Float32(), False), + pytest.param(Float64(), Float(64), True), + ], +) +def test_floats_eq(float_1: Float, float_2: Float, expected_equal: bool): + """Test float eq""" + assert expected_equal == (float_1 == float_2) + assert expected_equal == (float_2 == float_1) From c9c7acf616684d04aa74a4cd0de1d2f01acf4296 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 5 Aug 2021 15:32:57 +0200 Subject: [PATCH 0051/1104] dev(floats): update function to mix types to support floats --- hdk/common/data_types/dtypes_helpers.py | 34 +++++++++++++------ .../common/data_types/test_dtypes_helpers.py | 30 ++++++++++++++++ 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index a7036b950..1c93737e6 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -1,12 +1,16 @@ """File to hold helper functions for data types related stuff""" +from copy import deepcopy from typing import cast from .base import BaseDataType +from .floats import Float from .integers import Integer from .values import BaseValue, ClearValue, EncryptedValue INTEGER_TYPES = (Integer,) +FLOAT_TYPES = (Float,) +SUPPORTED_TYPES = INTEGER_TYPES + FLOAT_TYPES def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: @@ -57,36 +61,44 @@ def find_type_to_hold_both_lossy( Returns: BaseDataType: The dtype able to hold (potentially lossy) dtype1 and dtype2 """ + assert isinstance(dtype1, SUPPORTED_TYPES), f"Unsupported dtype1: {type(dtype1)}" + assert isinstance(dtype2, SUPPORTED_TYPES), f"Unsupported dtype2: {type(dtype2)}" + + type_to_return: BaseDataType + if isinstance(dtype1, Integer) and isinstance(dtype2, Integer): d1_signed = dtype1.is_signed d2_signed = dtype2.is_signed max_bits = max(dtype1.bit_width, dtype2.bit_width) - holding_integer: BaseDataType - if d1_signed and d2_signed: - holding_integer = Integer(max_bits, is_signed=True) + type_to_return = Integer(max_bits, is_signed=True) elif not d1_signed and not d2_signed: - holding_integer = Integer(max_bits, is_signed=False) + type_to_return = Integer(max_bits, is_signed=False) elif d1_signed and not d2_signed: # 2 is unsigned, if it has the bigger bit_width, we need a signed integer that can hold # it, so add 1 bit of sign to its bit_width if dtype2.bit_width >= dtype1.bit_width: new_bit_width = dtype2.bit_width + 1 - holding_integer = Integer(new_bit_width, is_signed=True) + type_to_return = Integer(new_bit_width, is_signed=True) else: - holding_integer = Integer(dtype1.bit_width, is_signed=True) + type_to_return = Integer(dtype1.bit_width, is_signed=True) elif not d1_signed and d2_signed: # Same as above, with 1 and 2 switched around if dtype1.bit_width >= dtype2.bit_width: new_bit_width = dtype1.bit_width + 1 - holding_integer = Integer(new_bit_width, is_signed=True) + type_to_return = Integer(new_bit_width, is_signed=True) else: - holding_integer = Integer(dtype2.bit_width, is_signed=True) + type_to_return = Integer(dtype2.bit_width, is_signed=True) + elif isinstance(dtype1, Float) and isinstance(dtype2, Float): + max_bits = max(dtype1.bit_width, dtype2.bit_width) + type_to_return = Float(max_bits) + elif isinstance(dtype1, Float): + type_to_return = deepcopy(dtype1) + elif isinstance(dtype2, Float): + type_to_return = deepcopy(dtype2) - return holding_integer - - raise NotImplementedError("For now only Integers are supported by find_type_to_hold_both_lossy") + return type_to_return def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> BaseValue: diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index d9fc88099..c66fbae88 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -9,6 +9,7 @@ from hdk.common.data_types.dtypes_helpers import ( value_is_encrypted_integer, value_is_encrypted_unsigned_integer, ) +from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import BaseValue, ClearValue, EncryptedValue @@ -73,6 +74,16 @@ class UnsupportedDataType(BaseDataType): pytest.param(Integer(6, False), Integer(6, True), Integer(7, True), id="uint6, int6, int7"), pytest.param(Integer(6, True), Integer(5, False), Integer(6, True), id="int6, uint5, int6"), pytest.param(Integer(5, False), Integer(6, True), Integer(6, True), id="uint5, int6, int6"), + pytest.param(Integer(32, True), Float(32), Float(32), id="int32, float32, float32"), + pytest.param(Integer(64, True), Float(32), Float(32), id="int64, float32, float32"), + pytest.param(Integer(64, True), Float(64), Float(64), id="int64, float64, float64"), + pytest.param(Integer(32, True), Float(64), Float(64), id="int32, float64, float64"), + pytest.param(Float(64), Integer(32, True), Float(64), id="float64, int32, float64"), + pytest.param(Float(64), Integer(7, False), Float(64), id="float64, uint7, float64"), + pytest.param(Float(32), Float(32), Float(32), id="float32, float32, float32"), + pytest.param(Float(32), Float(64), Float(64), id="float32, float64, float64"), + pytest.param(Float(64), Float(32), Float(64), id="float64, float32, float64"), + pytest.param(Float(64), Float(64), Float(64), id="float64, float64, float64"), pytest.param( UnsupportedDataType(), UnsupportedDataType(), @@ -94,6 +105,13 @@ class UnsupportedDataType(BaseDataType): id="unsupported, int6, xfail", marks=pytest.mark.xfail(strict=True), ), + pytest.param( + UnsupportedDataType(), + Float(32), + None, + id="unsupported, float32, xfail", + marks=pytest.mark.xfail(strict=True), + ), ], ) def test_mix_data_types( @@ -132,6 +150,18 @@ def test_mix_data_types( ClearValue(Integer(7, False)), id="cuint7, cuint7, cuint7", ), + pytest.param( + ClearValue(Float(32)), + ClearValue(Float(32)), + ClearValue(Float(32)), + id="cfloat32, cfloat32, cfloat32", + ), + pytest.param( + EncryptedValue(Float(32)), + ClearValue(Float(32)), + EncryptedValue(Float(32)), + id="efloat32, cfloat32, efloat32", + ), ], ) def test_mix_values(value1: BaseValue, value2: BaseValue, expected_mixed_value: BaseValue): From 7fa67e1e4a2913139eddaf9cb55f37c32ab870e3 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 5 Aug 2021 14:24:30 +0300 Subject: [PATCH 0052/1104] doc: create getting started guide --- docs/dev/GETTING-STARTED.md | 92 +++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/dev/GETTING-STARTED.md diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md new file mode 100644 index 000000000..e75143271 --- /dev/null +++ b/docs/dev/GETTING-STARTED.md @@ -0,0 +1,92 @@ +# Getting Started + +## Preparation + +### Installing Python v3.8 + +You can follow [this](https://realpython.com/installing-python/) guide. + +### Installing Poertry + +You can follow [this](https://python-poetry.org/docs/#installation) guide. + +### Cloning repository + +```shell +git clone https://github.com/zama-ai/hdk.git +``` + +### Setting up environment + +```shell +cd hdk +make setup_env +``` + +### Activating the environment + +```shell +source .venv/bin/activate +``` + +### Syncing environment with the latest changes + +```shell +make sync_env +``` + +## Module Structure + +- hdk + - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) + - bounds_measurement: utilities for determining bounds of intermediate representation + - data_types: type definitions of typing information of intermediate representation + - debugging: utilities for printing/displaying intermediate representation + - representation: type definitions of intermediate representation + - tracing: utilities for generic function tracing used during intermediate representation creation + - hnumpy: numpy frontend of hdk + +## Contributing + +### Creating a new branch + +```shell +git checkout -b (feat|fix|refactor|test|benchmark|doc|chore)/short-description +``` + +e.g. + +```shell +git checkout -b feat/explicit-tlu +``` + +### Before committing + +Each commit to `hdk` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. + +```shell +make -k pytest +make -k pcc +``` + +### Before creating pull request + +Commits on the latest version of `main` branch should be rebased to your branch before your PR can be accepted. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. + +```shell +git checkout main +git pull +git checkout $YOUR_BRANCH +git rebase main +git push --force +``` + +You can learn more about rebasing in [here](https://git-scm.com/docs/git-rebase). + +The last requirement before creating your PR is to make sure you get a hundred percent code coverage. You can verify this using the following command. + +```shell +BB=$YOUR_BRANCH make coverage +``` + +If your coverage is below hundred percent, you should write more tests and then create the pull request. From e0231d97759707196af6bac936d8d9bef6aa8991 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 5 Aug 2021 16:30:46 +0300 Subject: [PATCH 0053/1104] doc(getting-started-guide): create terminology section, add section descriptions, improve some commands, fix typos --- docs/dev/GETTING-STARTED.md | 84 +++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index e75143271..6f1f682da 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -2,22 +2,32 @@ ## Preparation +Before you can start improving `hdk` you need to set up your development environment! This section will show how you can do that. + ### Installing Python v3.8 -You can follow [this](https://realpython.com/installing-python/) guide. +`hdk` is a `Python` library. So `Python` should be installed to develop `hdk`. `v3.8` is recommended because our CI also uses `v3.8`. -### Installing Poertry +You can follow [this](https://realpython.com/installing-python/) guide to install it (alternatively you can google `how to install python 3.8`). -You can follow [this](https://python-poetry.org/docs/#installation) guide. +### Installing Poetry + +`Poetry` is our package manager. It simplifies depenency and environment management by a lot. + +You can follow [this](https://python-poetry.org/docs/#installation) guide to install it (this is the official guide so just use it). ### Cloning repository +Now, it's time to get the source code of `hdk`. You can use the following command to do that. + ```shell git clone https://github.com/zama-ai/hdk.git ``` ### Setting up environment +We are going to make use of virtual environments. This helps to keep the project isolated from other `Python` projects in the system. The following commands will create a new virtual environment under the project directory and install dependencies to it. + ```shell cd hdk make setup_env @@ -25,18 +35,38 @@ make setup_env ### Activating the environment +Finally, all we need to do is to activate the newly created environment using the following command. + ```shell source .venv/bin/activate ``` ### Syncing environment with the latest changes +From time to time, new dependencies will be added to project or the old ones will be removed. The command below will make sure the project have proper environment. So run it regularly! + ```shell make sync_env ``` +## Terminology + +In this section we will go over some terms that we use throughout the project. + +- intermediate representation + - a data structure to represent a calculation + - basically a computation graph where nodes are either inputs or operations +- tracing + - act of creating intermediate representation from plain python functions + - this is awesome to have to avoid manual intermediate representation creation +- bounds + - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) + - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node + ## Module Structure +In this section, we will discuss the module structure of hdk briefly. You are encouraged to check individual `.py` files to learn more! + - hdk - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) - bounds_measurement: utilities for determining bounds of intermediate representation @@ -48,16 +78,21 @@ make sync_env ## Contributing +Now, you have a working environment, and you know what is where in the project. You are ready to contribute! Well, not so fast let's go over some other important things that you need to be careful about. + ### Creating a new branch +We are using a consistent branch naming schema, and you are expected to follow it as well. Here is the format and some examples. + ```shell -git checkout -b (feat|fix|refactor|test|benchmark|doc|chore)/short-description +git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description{_$issue_id}? ``` e.g. ```shell git checkout -b feat/explicit-tlu +git checkout -b fix/tracing_indexing_42 ``` ### Before committing @@ -65,19 +100,47 @@ git checkout -b feat/explicit-tlu Each commit to `hdk` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. ```shell -make -k pytest make -k pcc +make pytest +``` + +### Commiting + +We are using a consistent commit naming schema, and you are expected to follow it as well. Here is the format and some examples. + +```shell +git commit -m "{feat|fix|refactor|test|benchmark|doc|style|chore}{($location)}?: description of the change" +``` + +e.g. + +```shell +git commit -m "feat: implement bounds checking" +git commit -m "feat(debugging): add an helper function to draw intermediate representation" +git commit -m "fix(tracing): fix a bug that crashed pytorch tracer" ``` ### Before creating pull request -Commits on the latest version of `main` branch should be rebased to your branch before your PR can be accepted. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. +You should rebase on top of `main` branch before you create your pull request. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. ```shell +# fetch the list of active remote branches +git fetch --all --prune + +# checkout to main git checkout main -git pull + +# pull the latest changes to main (--ff-only is there to prevent accidental commits to main) +git pull --ff-only + +# checkout back to your branch git checkout $YOUR_BRANCH + +# rebase on top of main branch git rebase main + +# push the latest version of the local branch to remote git push --force ``` @@ -86,7 +149,10 @@ You can learn more about rebasing in [here](https://git-scm.com/docs/git-rebase) The last requirement before creating your PR is to make sure you get a hundred percent code coverage. You can verify this using the following command. ```shell -BB=$YOUR_BRANCH make coverage +make pytest +make coverage ``` -If your coverage is below hundred percent, you should write more tests and then create the pull request. +Note that this will compare the coverage with `origin/main`. If you want to set a custom base branch, you can specify `BB` environment variable like so `BB=$YOUR_BASE_BRANCH make coverage`. + +If your coverage is below hundred percent, you should write more tests and then create the pull request. If you ignore this warning and create the PR, GitHub actions will fail and your PR will not be merged anyway. From 43ba1c5296dde775f17812ea1e8a031c2f5c7247 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 5 Aug 2021 16:35:35 +0300 Subject: [PATCH 0054/1104] doc(getting-started-guide): add link to conventional commits documentation --- docs/dev/GETTING-STARTED.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 6f1f682da..05d713c59 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -120,6 +120,8 @@ git commit -m "feat(debugging): add an helper function to draw intermediate repr git commit -m "fix(tracing): fix a bug that crashed pytorch tracer" ``` +To learn more about it, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page. + ### Before creating pull request You should rebase on top of `main` branch before you create your pull request. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. From ebce33327de226b27d5f20c102dc3247d593ac49 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 5 Aug 2021 17:42:08 +0300 Subject: [PATCH 0055/1104] doc(getting-started-guide): rephrase some sentences, fix typos, add leaving the environment section --- docs/dev/GETTING-STARTED.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 05d713c59..48735fe40 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -12,9 +12,9 @@ You can follow [this](https://realpython.com/installing-python/) guide to instal ### Installing Poetry -`Poetry` is our package manager. It simplifies depenency and environment management by a lot. +`Poetry` is our package manager. It simplifies dependency and environment management by a lot. -You can follow [this](https://python-poetry.org/docs/#installation) guide to install it (this is the official guide so just use it). +You can follow [this](https://python-poetry.org/docs/#installation) official guide to install it. ### Cloning repository @@ -41,6 +41,14 @@ Finally, all we need to do is to activate the newly created environment using th source .venv/bin/activate ``` +### Leaving the environment + +After your work is done you can simply run the following command to leave the environment. + +```shell +deactivate +``` + ### Syncing environment with the latest changes From time to time, new dependencies will be added to project or the old ones will be removed. The command below will make sure the project have proper environment. So run it regularly! @@ -55,13 +63,13 @@ In this section we will go over some terms that we use throughout the project. - intermediate representation - a data structure to represent a calculation - - basically a computation graph where nodes are either inputs or operations + - basically a computation graph where nodes are either inputs or operations on other nodes - tracing - act of creating intermediate representation from plain python functions - this is awesome to have to avoid manual intermediate representation creation - bounds - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) - - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node + - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node ## Module Structure @@ -82,7 +90,7 @@ Now, you have a working environment, and you know what is where in the project. ### Creating a new branch -We are using a consistent branch naming schema, and you are expected to follow it as well. Here is the format and some examples. +We are using a consistent branch naming scheme, and you are expected to follow it as well. Here is the format and some examples. ```shell git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description{_$issue_id}? @@ -106,7 +114,7 @@ make pytest ### Commiting -We are using a consistent commit naming schema, and you are expected to follow it as well. Here is the format and some examples. +We are using a consistent commit naming scheme, and you are expected to follow it as well. Here is the format and some examples. ```shell git commit -m "{feat|fix|refactor|test|benchmark|doc|style|chore}{($location)}?: description of the change" @@ -120,7 +128,7 @@ git commit -m "feat(debugging): add an helper function to draw intermediate repr git commit -m "fix(tracing): fix a bug that crashed pytorch tracer" ``` -To learn more about it, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page. +To learn more about conventional commits, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page. ### Before creating pull request From 4b1990e731404ac505d12779a1b2ff9ef8e81a74 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 5 Aug 2021 18:06:48 +0300 Subject: [PATCH 0056/1104] doc(getting-started-guide): add getting started guide to sphinx toc and readme.md, fix branch convention, improve wording --- README.md | 3 ++- docs/dev/GETTING-STARTED.md | 7 +++---- docs/index.rst | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d40e3f482..4d29a43f7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # hdk + Homomorphic Development Framework - collection of tools to FHE all the things -Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) +Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) to get the bigger picture and [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to start developing. diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 48735fe40..dc4241d32 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -65,8 +65,7 @@ In this section we will go over some terms that we use throughout the project. - a data structure to represent a calculation - basically a computation graph where nodes are either inputs or operations on other nodes - tracing - - act of creating intermediate representation from plain python functions - - this is awesome to have to avoid manual intermediate representation creation + - it is our technique to take directly a plain numpy function from a user and deduce its intermediate representation in a painless way for the user - bounds - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node @@ -93,13 +92,13 @@ Now, you have a working environment, and you know what is where in the project. We are using a consistent branch naming scheme, and you are expected to follow it as well. Here is the format and some examples. ```shell -git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description{_$issue_id}? +git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description_$issue_id ``` e.g. ```shell -git checkout -b feat/explicit-tlu +git checkout -b feat/explicit-tlu_11 git checkout -b fix/tracing_indexing_42 ``` diff --git a/docs/index.rst b/docs/index.rst index 7a66c24a8..51da0dcdb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,4 +6,5 @@ Homomorphic Development Kit's documentation :maxdepth: 2 :caption: Developer docs - dev/ARCHITECTURE.md \ No newline at end of file + dev/ARCHITECTURE.md + dev/GETTING-STARTED.md From 669cf48ea47a15497e44ef9ce0055824833319da Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 5 Aug 2021 18:08:02 +0300 Subject: [PATCH 0057/1104] doc(README.md): fix double spacing error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d29a43f7..12b9c9443 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Homomorphic Development Framework - collection of tools to FHE all the things -Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) to get the bigger picture and [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to start developing. +Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) to get the bigger picture and [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to start developing. From b79fd775d041591e43df5c742beecb20c6e898b1 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 6 Aug 2021 10:17:10 +0300 Subject: [PATCH 0058/1104] doc(readme): fix a grammatical mistake --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12b9c9443..5345e6cf5 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ Homomorphic Development Framework - collection of tools to FHE all the things -Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) to get the bigger picture and [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to start developing. +Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) to get the big picture and [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to start developing. From ee832079ba1355556d0d63545c4fa2d96ac0ba8b Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 6 Aug 2021 15:13:45 +0200 Subject: [PATCH 0059/1104] fix: remove the use of map in the code base closes #90 --- hdk/common/common_helpers.py | 4 ++-- hdk/common/data_types/integers.py | 2 +- hdk/common/representation/intermediate.py | 2 +- hdk/common/tracing/base_tracer.py | 2 +- tests/common/bounds_measurement/test_dataset_eval.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hdk/common/common_helpers.py b/hdk/common/common_helpers.py index e0c872c9a..c0f2d8fdf 100644 --- a/hdk/common/common_helpers.py +++ b/hdk/common/common_helpers.py @@ -16,8 +16,8 @@ def ir_nodes_has_integer_input_and_output(node: ir.IntermediateNode) -> bool: Returns: bool: True if all input and output values hold Integers """ - return all(map(lambda x: isinstance(x.data_type, Integer), node.inputs)) and all( - map(lambda x: isinstance(x.data_type, Integer), node.outputs) + return all(isinstance(x.data_type, Integer) for x in node.inputs) and all( + isinstance(x.data_type, Integer) for x in node.outputs ) diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index 49c04e23f..f4753f09b 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -94,7 +94,7 @@ def make_integer_to_hold_ints(values: Iterable[int], force_signed: bool) -> Inte Returns: Integer: The Integer able to hold values """ - assert all(map(lambda x: isinstance(x, int), values)) + assert all(isinstance(x, int) for x in values) min_value = min(values) max_value = max(values) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 163b87d60..db78a6e4f 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -26,7 +26,7 @@ class IntermediateNode(ABC): op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: self.inputs = list(inputs) - assert all(map(lambda x: isinstance(x, BaseValue), self.inputs)) + assert all(isinstance(x, BaseValue) for x in self.inputs) self.op_args = op_args self.op_kwargs = op_kwargs diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 3519f30d4..188f30e16 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -55,7 +55,7 @@ class BaseTracer(ABC): sanitized_inputs = [sanitize(inp) for inp in inputs] traced_computation = computation_to_trace( - map(lambda x: x.output, sanitized_inputs), + (x.output for x in sanitized_inputs), op_args=op_args, op_kwargs=op_kwargs, ) diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index f2e331412..cdf30691a 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -213,7 +213,7 @@ def test_eval_op_graph_bounds_on_dataset_multiple_output( yield (x_gen, y_gen) node_bounds = eval_op_graph_bounds_on_dataset( - op_graph, data_gen(*tuple(map(lambda x: range(x[0], x[1] + 1), input_ranges))) + op_graph, data_gen(*tuple(range(x[0], x[1] + 1) for x in input_ranges)) ) for i, output_node in op_graph.output_nodes.items(): From 36e30e81b43fb43c8bb7be10dae2b9ec9ea0d525 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 6 Aug 2021 11:06:18 +0200 Subject: [PATCH 0060/1104] fix: check datasets closes #91 --- hdk/common/bounds_measurement/dataset_eval.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/hdk/common/bounds_measurement/dataset_eval.py index d30dda021..46e588821 100644 --- a/hdk/common/bounds_measurement/dataset_eval.py +++ b/hdk/common/bounds_measurement/dataset_eval.py @@ -18,6 +18,14 @@ def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, data_generator: Iterator) and a dict with keys "min" and "max" as value """ first_input_data = dict(enumerate(next(data_generator))) + + # Check the dataset is well-formed + assert len(first_input_data) == len(op_graph.input_nodes), ( + f"Got input data from dataset of len: {len(first_input_data)}, function being evaluated has" + f" only {len(op_graph.input_nodes)} inputs, please make sure your data generator returns" + f" valid tuples of input values" + ) + first_output = op_graph.evaluate(first_input_data) node_bounds = { @@ -26,7 +34,19 @@ def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, data_generator: Iterator) } for input_data in data_generator: - current_output = op_graph.evaluate(dict(enumerate(input_data))) + + next_input_data = dict(enumerate(input_data)) + + # Check the dataset is well-formed + assert len(next_input_data) == len(op_graph.input_nodes), ( + f"Got input data from dataset of len: {len(next_input_data)}," + f" function being evaluated has" + f" only {len(op_graph.input_nodes)} inputs, please make sure" + f" your data generator returns" + f" valid tuples of input values" + ) + + current_output = op_graph.evaluate(next_input_data) for node, value in current_output.items(): node_bounds[node]["min"] = min(node_bounds[node]["min"], value) node_bounds[node]["max"] = max(node_bounds[node]["max"], value) From 055298daf8f58b1b66f279220bd414e31ebc2460 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 6 Aug 2021 11:23:01 +0200 Subject: [PATCH 0061/1104] doc: explain what are datasets in eval_op_graph_bounds_on_dataset --- hdk/common/bounds_measurement/dataset_eval.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/hdk/common/bounds_measurement/dataset_eval.py index 46e588821..4abd8d719 100644 --- a/hdk/common/bounds_measurement/dataset_eval.py +++ b/hdk/common/bounds_measurement/dataset_eval.py @@ -1,23 +1,25 @@ """Code to evaluate the IR graph on datasets""" -from typing import Iterator +from typing import Any, Iterator, Tuple from ..operator_graph import OPGraph -def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, data_generator: Iterator): +def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, dataset: Iterator[Tuple[Any, ...]]): """Evaluate the bounds for all output values of the operators in the graph op_graph over data - coming from the data_generator + coming from the dataset Args: op_graph (OPGraph): The graph for which we want to determine the bounds - data_generator (Iterator): The dataset over which op_graph is evaluated + dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + needs to be an iterator on tuples which are of the same length than the number of + parameters in the function, and in the same order than these same parameters Returns: Dict: dict containing the bounds for each node from op_graph, stored with the node as key and a dict with keys "min" and "max" as value """ - first_input_data = dict(enumerate(next(data_generator))) + first_input_data = dict(enumerate(next(dataset))) # Check the dataset is well-formed assert len(first_input_data) == len(op_graph.input_nodes), ( @@ -33,7 +35,7 @@ def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, data_generator: Iterator) for node in op_graph.graph.nodes() } - for input_data in data_generator: + for input_data in dataset: next_input_data = dict(enumerate(input_data)) From 6491e47178033f4c566848bef9ce7f2905a34e80 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 5 Aug 2021 17:36:25 +0200 Subject: [PATCH 0062/1104] feat: adding a compilation api also, showing data_types in get_printable_graph refs #86, #87 --- hdk/common/debugging/draw_graph.py | 26 +++++++++++++++-- hdk/hnumpy/compile.py | 42 ++++++++++++++++++++++++++ tests/hnumpy/test_compile.py | 47 ++++++++++++++++++++++++++++++ tests/hnumpy/test_debugging.py | 43 +++++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 hdk/hnumpy/compile.py create mode 100644 tests/hnumpy/test_compile.py diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index 9d404c94c..ec6a8038c 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -219,11 +219,26 @@ def draw_graph( # pylint: enable=too-many-locals -def get_printable_graph(opgraph: OPGraph) -> str: +def data_type_to_string(node): + """Return the datatypes of the outputs of the node + + Args: + node: a graph node + + Returns: + str: a string representing the datatypes of the outputs of the node + + """ + return ", ".join([str(o.data_type) for o in node.outputs]) + + +def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: """Return a string representing a graph Args: graph (OPGraph): The graph that we want to draw + show_data_types (bool): Whether or not showing data_types of nodes, eg + to see their width Returns: str: a string to print or save in a file @@ -265,7 +280,14 @@ def get_printable_graph(opgraph: OPGraph) -> str: list_of_arg_name.sort() what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" - returned_str += f"\n%{i} = {what_to_print}" + new_line = f"%{i} = {what_to_print}" + + # Manage datatypes + if show_data_types: + new_line = f"{new_line: <40s} # {data_type_to_string(node)}" + + returned_str += f"\n{new_line}" + map_table[node] = i i += 1 diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py new file mode 100644 index 000000000..0882aa6bd --- /dev/null +++ b/hdk/hnumpy/compile.py @@ -0,0 +1,42 @@ +"""hnumpy compilation function""" + +from typing import Any, Callable, Dict, Iterator, Tuple + +from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset +from hdk.hnumpy.tracing import trace_numpy_function + +from ..common.data_types import BaseValue +from ..common.operator_graph import OPGraph +from ..hnumpy.tracing import trace_numpy_function + + +def compile_numpy_function( + function_to_trace: Callable, + function_parameters: Dict[str, BaseValue], + dataset: Iterator[Tuple[Any, ...]], +) -> OPGraph: + """Main API of hnumpy, to be able to compile an homomorphic program + + Args: + function_to_trace (Callable): The function you want to trace + function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the + function is e.g. an EncryptedValue holding a 7bits unsigned Integer + dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + needs to be an iterator on tuples which are of the same length than the number of + parameters in the function, and in the same order than these same parameters + + Returns: + OPGraph: currently returns a compilable graph, but later, it will return an MLIR compatible + with the compiler, and even later, it will return the result of the compilation + """ + + # Trace + op_graph = trace_numpy_function(function_to_trace, function_parameters) + + # Find bounds with the dataset + node_bounds = eval_op_graph_bounds_on_dataset(op_graph, dataset) + + # Update the graph accordingly: after that, we have the compilable graph + op_graph.update_values_with_bounds(node_bounds) + + return op_graph diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py new file mode 100644 index 000000000..fb7a76147 --- /dev/null +++ b/tests/hnumpy/test_compile.py @@ -0,0 +1,47 @@ +"""Test file for hnumpy compilation functions""" +import itertools + +import pytest + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.common.debugging import draw_graph, get_printable_graph +from hdk.hnumpy.compile import compile_numpy_function + + +@pytest.mark.parametrize( + "function,input_ranges,list_of_arg_names", + [ + pytest.param(lambda x: x + 42, ((-2, 2),), ["x"]), + pytest.param(lambda x, y: x + y + 8, ((-10, 2), (-4, 6)), ["x", "y"]), + pytest.param(lambda x, y: (x + 1, y + 10), ((-1, 1), (3, 4)), ["x", "y"]), + pytest.param( + lambda x, y, z: (x + y + 1 - z, x * y + 42, z, z + 99), + ((-1, 1), (3, 4), (10, 20)), + ["x", "y", "z"], + ), + ], +) +def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_names): + """Test function compile_numpy_function for a program with multiple outputs""" + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedValue(Integer(64, True)) for arg_name in list_of_arg_names + } + + op_graph = compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + # TODO: For the moment, we don't have really checks, but some printfs. Later, + # when we have the converter, we can check the MLIR + draw_graph(op_graph, block_until_user_closes_graph=False) + + str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) + print(f"\n{str_of_the_graph}\n") diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index ea611a791..750f56bbd 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -96,3 +96,46 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): print(f"\nExp {ref_graph_str}\n") assert str_of_the_graph == ref_graph_str + + +# Remark that the bitwidths are not particularly correct (eg, a MUL of a 17b times 23b +# returning 23b), since they are replaced later by the real bitwidths computed on the +# dataset +@pytest.mark.parametrize( + "lambda_f,x_y,ref_graph_str", + [ + ( + lambda x, y: x + y, + ( + EncryptedValue(Integer(64, is_signed=False)), + EncryptedValue(Integer(32, is_signed=True)), + ), + "\n%0 = x # Integer" + "\n%1 = y # Integer" + "\n%2 = Add(0, 1) # Integer" + "\nreturn(%2)", + ), + ( + lambda x, y: x * y, + ( + EncryptedValue(Integer(17, is_signed=False)), + EncryptedValue(Integer(23, is_signed=False)), + ), + "\n%0 = x # Integer" + "\n%1 = y # Integer" + "\n%2 = Mul(0, 1) # Integer" + "\nreturn(%2)", + ), + ], +) +def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): + "Test hnumpy get_printable_graph with show_data_types" + x, y = x_y + graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) + + str_of_the_graph = get_printable_graph(graph, show_data_types=True) + + print(f"\nGot {str_of_the_graph}\n") + print(f"\nExp {ref_graph_str}\n") + + assert str_of_the_graph == ref_graph_str From 789a9766614b1f4a67d58ceca92cfd27d78f09e3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 5 Aug 2021 18:24:36 +0200 Subject: [PATCH 0063/1104] dev(floats): add the possibility to have constant floats in a program - update ConstantInput to manage floats - update OPGraph update_values_with_bounds to manage floats - update test code to manage cases where output could be a float - add test cases with float inputs --- hdk/common/bounds_measurement/dataset_eval.py | 38 ++++----- hdk/common/operator_graph.py | 15 +++- hdk/common/representation/intermediate.py | 17 ++-- .../bounds_measurement/test_dataset_eval.py | 80 +++++++++++++++++-- 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/hdk/common/bounds_measurement/dataset_eval.py index 4abd8d719..8fb8d5df9 100644 --- a/hdk/common/bounds_measurement/dataset_eval.py +++ b/hdk/common/bounds_measurement/dataset_eval.py @@ -19,15 +19,21 @@ def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, dataset: Iterator[Tuple[A Dict: dict containing the bounds for each node from op_graph, stored with the node as key and a dict with keys "min" and "max" as value """ + + def check_dataset_input_is_valid(data_to_check): + assert len(data_to_check) == len(op_graph.input_nodes), ( + f"Got input data from dataset of len: {len(data_to_check)}, " + f"function being evaluated has {len(op_graph.input_nodes)} inputs, please make " + f"sure your data generator returns valid tuples of input values" + ) + # TODO: change this to be more generic and check coherence between the input data type and + # the corresponding Input ir node expected data type + assert all( + isinstance(val, int) for val in data_to_check + ), "For now dataset evaluation only support int as inputs, please check your dataset" + first_input_data = dict(enumerate(next(dataset))) - - # Check the dataset is well-formed - assert len(first_input_data) == len(op_graph.input_nodes), ( - f"Got input data from dataset of len: {len(first_input_data)}, function being evaluated has" - f" only {len(op_graph.input_nodes)} inputs, please make sure your data generator returns" - f" valid tuples of input values" - ) - + check_dataset_input_is_valid(first_input_data.values()) first_output = op_graph.evaluate(first_input_data) node_bounds = { @@ -36,19 +42,9 @@ def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, dataset: Iterator[Tuple[A } for input_data in dataset: - - next_input_data = dict(enumerate(input_data)) - - # Check the dataset is well-formed - assert len(next_input_data) == len(op_graph.input_nodes), ( - f"Got input data from dataset of len: {len(next_input_data)}," - f" function being evaluated has" - f" only {len(op_graph.input_nodes)} inputs, please make sure" - f" your data generator returns" - f" valid tuples of input values" - ) - - current_output = op_graph.evaluate(next_input_data) + current_input_data = dict(enumerate(input_data)) + check_dataset_input_is_valid(current_input_data.values()) + current_output = op_graph.evaluate(current_input_data) for node, value in current_output.items(): node_bounds[node]["min"] = min(node_bounds[node]["min"], value) node_bounds[node]["max"] = max(node_bounds[node]["max"], value) diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index 4a93182c1..04655eeba 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Iterable, Mapping import networkx as nx +from .data_types.floats import Float from .data_types.integers import make_integer_to_hold_ints from .representation import intermediate as ir from .tracing import BaseTracer @@ -71,10 +72,18 @@ class OPGraph: if not isinstance(node, ir.Input): for output_value in node.outputs: - output_value.data_type = make_integer_to_hold_ints( - (min_bound, max_bound), force_signed=False - ) + if isinstance(min_bound, int) and isinstance(max_bound, int): + output_value.data_type = make_integer_to_hold_ints( + (min_bound, max_bound), force_signed=False + ) + else: + output_value.data_type = Float(64) else: + # Currently variable inputs are only allowed to be integers + assert isinstance(min_bound, int) and isinstance(max_bound, int), ( + f"Inputs to a graph should be integers, got bounds that were not float, \n" + f"min: {min_bound} ({type(min_bound)}), max: {max_bound} ({type(max_bound)})" + ) node.inputs[0].data_type = make_integer_to_hold_ints( (min_bound, max_bound), force_signed=False ) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index db78a6e4f..d5660a205 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple from ..data_types import BaseValue from ..data_types.dtypes_helpers import mix_values_determine_holding_dtype +from ..data_types.floats import Float from ..data_types.integers import Integer, get_bits_to_represent_int from ..data_types.scalars import Scalars from ..data_types.values import ClearValue @@ -153,12 +154,16 @@ class ConstantInput(IntermediateNode): super().__init__([]) self.constant_data = constant_data - # TODO: manage other cases, we can't call get_bits_to_represent_int - assert isinstance(constant_data, int) - is_signed = constant_data < 0 - self.outputs = [ - ClearValue(Integer(get_bits_to_represent_int(constant_data, is_signed), is_signed)) - ] + assert isinstance( + constant_data, (int, float) + ), "Only int and float are support for constant input" + if isinstance(constant_data, int): + is_signed = constant_data < 0 + self.outputs = [ + ClearValue(Integer(get_bits_to_represent_int(constant_data, is_signed), is_signed)) + ] + elif isinstance(constant_data, float): + self.outputs = [ClearValue(Float(64))] def evaluate(self, inputs: Mapping[int, Any]) -> Any: return self.constant_data diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index cdf30691a..98f5b2b43 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -5,6 +5,7 @@ from typing import Tuple import pytest from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset +from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import EncryptedValue from hdk.hnumpy.tracing import trace_numpy_function @@ -27,6 +28,13 @@ from hdk.hnumpy.tracing import trace_numpy_function Integer(5, is_signed=True), id="x + y, (-10, 2), (-4, 5), (-14, 7)", ), + pytest.param( + lambda x, y: x + y + 1.7, + ((-10, 2), (-4, 5)), + (-12.3, 8.7), + Float(64), + id="x + y + 1.7, (-10, 2), (-4, 5), (-12.3, 8.7)", + ), pytest.param( lambda x, y: x + y + 1, ((-10, 2), (-4, 5)), @@ -67,21 +75,42 @@ from hdk.hnumpy.tracing import trace_numpy_function ((-10, 2), (-4, 5)), (-57, -36), Integer(7, is_signed=True), - id="x - y, (-10, 2), (-4, 5), (-57, -36)", + id="x - y - 42, (-10, 2), (-4, 5), (-57, -36)", + ), + pytest.param( + lambda x, y: x - y - 41.5, + ((-10, 2), (-4, 5)), + (-56.5, -35.5), + Float(64), + id="x - y - 41.5, (-10, 2), (-4, 5), (-56.5, -35.5)", ), pytest.param( lambda x, y: 3 - x + y, ((-10, 2), (-4, 5)), (-3, 18), Integer(6, is_signed=True), - id="x - y, (-10, 2), (-4, 5), (-3, 18)", + id="3 - x + y, (-10, 2), (-4, 5), (-3, 18)", + ), + pytest.param( + lambda x, y: 2.8 - x + y, + ((-10, 2), (-4, 5)), + (-3.2, 17.8), + Float(64), + id="2.8 - x + y, (-10, 2), (-4, 5), (-3.2, 17.8)", ), pytest.param( lambda x, y: (-13) - x + y, ((-10, 2), (-4, 5)), (-19, 2), Integer(6, is_signed=True), - id="x - y, (-10, 2), (-4, 5), (-16, 2)", + id="(-13) - x + y, (-10, 2), (-4, 5), (-19, 2)", + ), + pytest.param( + lambda x, y: (-13.5) - x + y, + ((-10, 2), (-4, 5)), + (-19.5, 1.5), + Float(64), + id="(-13.5) - x + y, (-10, 2), (-4, 5), (-19.5, 1.5)", ), pytest.param( lambda x, y: x * y, @@ -102,7 +131,14 @@ from hdk.hnumpy.tracing import trace_numpy_function ((-10, 2), (-4, 5)), (-150, 120), Integer(9, is_signed=True), - id="x * y, (-10, 2), (-4, 5), (-150, 120)", + id="(3 * x) * y, (-10, 2), (-4, 5), (-150, 120)", + ), + pytest.param( + lambda x, y: (3.0 * x) * y, + ((-10, 2), (-4, 5)), + (-150.0, 120.0), + Float(64), + id="(3.0 * x) * y, (-10, 2), (-4, 5), (-150.0, 120.0)", ), pytest.param( lambda x, y: (x * 11) * y, @@ -116,7 +152,14 @@ from hdk.hnumpy.tracing import trace_numpy_function ((-10, 2), (-4, 5)), (-440, 550), Integer(11, is_signed=True), - id="x * y, (-10, 2), (-4, 5), (-440, 550)", + id="(x * (-11)) * y, (-10, 2), (-4, 5), (-440, 550)", + ), + pytest.param( + lambda x, y: (x * (-11.0)) * y, + ((-10, 2), (-4, 5)), + (-440.0, 550.0), + Float(64), + id="(x * (-11.0)) * y, (-10, 2), (-4, 5), (-440.0, 550.0)", ), pytest.param( lambda x, y: x + x + y, @@ -187,12 +230,36 @@ def test_eval_op_graph_bounds_on_dataset( ((0, 2), (13, 14)), (Integer(2, is_signed=False), Integer(4, is_signed=False)), ), + pytest.param( + lambda x, y: (x + 1.5, y + 9.6), + ((-1, 1), (3, 4)), + ((0.5, 2.5), (12.6, 13.6)), + (Float(64), Float(64)), + ), pytest.param( lambda x, y: (x + y + 1, x * y + 42), ((-1, 1), (3, 4)), ((3, 6), (38, 46)), (Integer(3, is_signed=False), Integer(6, is_signed=False)), ), + pytest.param( + lambda x, y: (x + y + 0.4, x * y + 41.7), + ((-1, 1), (3, 4)), + ((2.4, 5.4), (37.7, 45.7)), + (Float(64), Float(64)), + ), + pytest.param( + lambda x, y: (x + y + 1, x * y + 41.7), + ((-1, 1), (3, 4)), + ((3, 6), (37.7, 45.7)), + (Integer(3, is_signed=False), Float(64)), + ), + pytest.param( + lambda x, y: (x + y + 0.4, x * y + 42), + ((-1, 1), (3, 4)), + ((2.4, 5.4), (38, 46)), + (Float(64), Integer(6, is_signed=False)), + ), ], ) def test_eval_op_graph_bounds_on_dataset_multiple_output( @@ -218,11 +285,8 @@ def test_eval_op_graph_bounds_on_dataset_multiple_output( for i, output_node in op_graph.output_nodes.items(): output_node_bounds = node_bounds[output_node] - assert (output_node_bounds["min"], output_node_bounds["max"]) == expected_output_bounds[i] - assert EncryptedValue(Integer(64, True)) == output_node.outputs[0] - op_graph.update_values_with_bounds(node_bounds) for i, output_node in op_graph.output_nodes.items(): From 5d9259c0003d1eca93423cc8f31c6a449e4e9a59 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 6 Aug 2021 15:04:34 +0200 Subject: [PATCH 0064/1104] dev(ir): add the ArbitraryFunction ir node --- hdk/common/representation/intermediate.py | 43 ++++++++++++++++--- .../representation/test_intermediate.py | 41 ++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index d5660a205..4b07e042d 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -2,14 +2,15 @@ from abc import ABC, abstractmethod from copy import deepcopy -from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple from ..data_types import BaseValue +from ..data_types.base import BaseDataType from ..data_types.dtypes_helpers import mix_values_determine_holding_dtype from ..data_types.floats import Float from ..data_types.integers import Integer, get_bits_to_represent_int from ..data_types.scalars import Scalars -from ..data_types.values import ClearValue +from ..data_types.values import ClearValue, EncryptedValue class IntermediateNode(ABC): @@ -17,8 +18,8 @@ class IntermediateNode(ABC): inputs: List[BaseValue] outputs: List[BaseValue] - op_args: Optional[Tuple[Any, ...]] - op_kwargs: Optional[Dict[str, Any]] + op_args: Tuple[Any, ...] + op_kwargs: Dict[str, Any] def __init__( self, @@ -28,8 +29,8 @@ class IntermediateNode(ABC): ) -> None: self.inputs = list(inputs) assert all(isinstance(x, BaseValue) for x in self.inputs) - self.op_args = op_args - self.op_kwargs = op_kwargs + self.op_args = deepcopy(op_args) if op_args is not None else () + self.op_kwargs = deepcopy(op_kwargs) if op_kwargs is not None else {} def _init_binary( self, @@ -167,3 +168,33 @@ class ConstantInput(IntermediateNode): def evaluate(self, inputs: Mapping[int, Any]) -> Any: return self.constant_data + + +class ArbitraryFunction(IntermediateNode): + """Node representing a univariate arbitrary function, e.g. sin(x)""" + + # The arbitrary_func is not optional but mypy has a long standing bug and is not able to + # understand this properly. See https://github.com/python/mypy/issues/708#issuecomment-605636623 + arbitrary_func: Optional[Callable] + + # pylint: disable=too-many-arguments + def __init__( + self, + input_base_value: BaseValue, + arbitrary_func: Callable, + output_dtype: BaseDataType, + op_args: Optional[Tuple[Any, ...]] = None, + op_kwargs: Optional[Dict[str, Any]] = None, + ) -> None: + super().__init__([input_base_value], op_args=op_args, op_kwargs=op_kwargs) + assert len(self.inputs) == 1 + self.arbitrary_func = arbitrary_func + # TLU/PBS has an encrypted output + self.outputs = [EncryptedValue(output_dtype)] + + # pylint: enable=too-many-arguments + + def evaluate(self, inputs: Mapping[int, Any]) -> Any: + # This is the continuation of the mypy bug workaround + assert self.arbitrary_func is not None + return self.arbitrary_func(inputs[0], *self.op_args, **self.op_kwargs) diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 2c4a665ec..530742be2 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -31,6 +31,47 @@ from hdk.common.representation import intermediate as ir pytest.param(ir.Input(ClearValue(Integer(32, True)), "in", 0), [42], 42, id="Input"), pytest.param(ir.ConstantInput(42), None, 42, id="ConstantInput"), pytest.param(ir.ConstantInput(-42), None, -42, id="ConstantInput"), + pytest.param( + ir.ArbitraryFunction( + EncryptedValue(Integer(7, False)), lambda x: x + 3, Integer(7, False) + ), + [10], + 13, + id="ArbitraryFunction, x + 3", + ), + pytest.param( + ir.ArbitraryFunction( + EncryptedValue(Integer(7, False)), + lambda x, y: x + y, + Integer(7, False), + op_kwargs={"y": 3}, + ), + [10], + 13, + id="ArbitraryFunction, (x, y) -> x + y, where y is constant == 3", + ), + pytest.param( + ir.ArbitraryFunction( + EncryptedValue(Integer(7, False)), + lambda x, y: y[x], + Integer(7, False), + op_kwargs={"y": (1, 2, 3, 4)}, + ), + [2], + 3, + id="ArbitraryFunction, (x, y) -> y[x], where y is constant == (1, 2, 3, 4)", + ), + pytest.param( + ir.ArbitraryFunction( + EncryptedValue(Integer(7, False)), + lambda x, y: y[3], + Integer(7, False), + op_kwargs={"y": (1, 2, 3, 4)}, + ), + [2], + 4, + id="ArbitraryFunction, x, y -> y[3], where y is constant == (1, 2, 3, 4)", + ), ], ) def test_evaluate( From d09f1b90a6e0a53f38d143ca82c4488dcb21ff50 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 9 Aug 2021 12:20:13 +0200 Subject: [PATCH 0065/1104] feat: let's be a bit more flexible with pylint closes #97 --- hdk/common/debugging/draw_graph.py | 9 --------- hdk/common/representation/intermediate.py | 3 --- pylintrc | 4 ++-- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index ec6a8038c..33cf49bd7 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -33,9 +33,6 @@ def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float """ - # FIXME: less variables - # pylint: disable=too-many-locals - nodes_depth = {node: 0 for node in graph.nodes()} input_nodes = [node for node in graph.nodes() if len(list(graph.predecessors(node))) == 0] @@ -83,7 +80,6 @@ def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float curr_x += x_delta - # pylint: enable=too-many-locals return pos @@ -109,9 +105,6 @@ def draw_graph( """ - # FIXME: less variables - # pylint: disable=too-many-locals - assert isinstance(opgraph, OPGraph) set_of_nodes_which_are_outputs = set(opgraph.output_nodes.values()) graph = opgraph.graph @@ -216,8 +209,6 @@ def draw_graph( # for CI plt.show(block=block_until_user_closes_graph) - # pylint: enable=too-many-locals - def data_type_to_string(node): """Return the datatypes of the outputs of the node diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 4b07e042d..11fdf1a3d 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -177,7 +177,6 @@ class ArbitraryFunction(IntermediateNode): # understand this properly. See https://github.com/python/mypy/issues/708#issuecomment-605636623 arbitrary_func: Optional[Callable] - # pylint: disable=too-many-arguments def __init__( self, input_base_value: BaseValue, @@ -192,8 +191,6 @@ class ArbitraryFunction(IntermediateNode): # TLU/PBS has an encrypted output self.outputs = [EncryptedValue(output_dtype)] - # pylint: enable=too-many-arguments - def evaluate(self, inputs: Mapping[int, Any]) -> Any: # This is the continuation of the mypy bug workaround assert self.arbitrary_func is not None diff --git a/pylintrc b/pylintrc index 388215226..d835e1277 100644 --- a/pylintrc +++ b/pylintrc @@ -545,7 +545,7 @@ valid-metaclass-classmethod-first-arg=cls [DESIGN] # Maximum number of arguments for function / method. -max-args=5 +max-args=10 # Maximum number of attributes for a class (see R0902). max-attributes=7 @@ -557,7 +557,7 @@ max-bool-expr=5 max-branches=12 # Maximum number of locals for function / method body. -max-locals=15 +max-locals=25 # Maximum number of parents for a class (see R0901). max-parents=7 From 371cdd5e6698b94318d7c32756dfcee2699de413 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 9 Aug 2021 10:09:05 +0200 Subject: [PATCH 0066/1104] fix(ir): make is_equivalent_to abstract - this allows to make sure each node has a proper implementation - move op_args and op_kwargs in ArbitraryFunction only - update BaseTracer accordingly --- hdk/common/representation/intermediate.py | 48 ++++--- hdk/common/tracing/base_tracer.py | 9 +- .../representation/test_intermediate.py | 119 ++++++++++++++++++ 3 files changed, 153 insertions(+), 23 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 11fdf1a3d..eef62fa8b 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -18,30 +18,20 @@ class IntermediateNode(ABC): inputs: List[BaseValue] outputs: List[BaseValue] - op_args: Tuple[Any, ...] - op_kwargs: Dict[str, Any] def __init__( self, inputs: Iterable[BaseValue], - op_args: Optional[Tuple[Any, ...]] = None, - op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: self.inputs = list(inputs) assert all(isinstance(x, BaseValue) for x in self.inputs) - self.op_args = deepcopy(op_args) if op_args is not None else () - self.op_kwargs = deepcopy(op_kwargs) if op_kwargs is not None else {} def _init_binary( self, inputs: Iterable[BaseValue], - op_args: Optional[Tuple[Any, ...]] = None, - op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: - assert op_args is None, f"Expected op_args to be None, got {op_args}" - assert op_kwargs is None, f"Expected op_kwargs to be None, got {op_kwargs}" - IntermediateNode.__init__(self, inputs, op_args=op_args, op_kwargs=op_kwargs) + IntermediateNode.__init__(self, inputs) assert len(self.inputs) == 2 @@ -61,6 +51,7 @@ class IntermediateNode(ABC): and self.outputs == other.outputs ) + @abstractmethod def is_equivalent_to(self, other: object) -> bool: """Overriding __eq__ has unwanted side effects, this provides the same facility without disrupting expected behavior too much @@ -72,11 +63,9 @@ class IntermediateNode(ABC): bool: True if the other object is equivalent """ return ( - isinstance(other, self.__class__) + isinstance(other, IntermediateNode) and self.inputs == other.inputs and self.outputs == other.outputs - and self.op_args == other.op_args - and self.op_kwargs == other.op_kwargs ) @abstractmethod @@ -142,6 +131,14 @@ class Input(IntermediateNode): def evaluate(self, inputs: Mapping[int, Any]) -> Any: return inputs[0] + def is_equivalent_to(self, other: object) -> bool: + return ( + isinstance(other, Input) + and self.input_name == other.input_name + and self.program_input_idx == other.program_input_idx + and super().is_equivalent_to(other) + ) + class ConstantInput(IntermediateNode): """Node representing a constant of the program""" @@ -169,6 +166,13 @@ class ConstantInput(IntermediateNode): def evaluate(self, inputs: Mapping[int, Any]) -> Any: return self.constant_data + def is_equivalent_to(self, other: object) -> bool: + return ( + isinstance(other, ConstantInput) + and self.constant_data == other.constant_data + and super().is_equivalent_to(other) + ) + class ArbitraryFunction(IntermediateNode): """Node representing a univariate arbitrary function, e.g. sin(x)""" @@ -176,6 +180,8 @@ class ArbitraryFunction(IntermediateNode): # The arbitrary_func is not optional but mypy has a long standing bug and is not able to # understand this properly. See https://github.com/python/mypy/issues/708#issuecomment-605636623 arbitrary_func: Optional[Callable] + op_args: Tuple[Any, ...] + op_kwargs: Dict[str, Any] def __init__( self, @@ -185,9 +191,11 @@ class ArbitraryFunction(IntermediateNode): op_args: Optional[Tuple[Any, ...]] = None, op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: - super().__init__([input_base_value], op_args=op_args, op_kwargs=op_kwargs) + super().__init__([input_base_value]) assert len(self.inputs) == 1 self.arbitrary_func = arbitrary_func + self.op_args = deepcopy(op_args) if op_args is not None else () + self.op_kwargs = deepcopy(op_kwargs) if op_kwargs is not None else {} # TLU/PBS has an encrypted output self.outputs = [EncryptedValue(output_dtype)] @@ -195,3 +203,13 @@ class ArbitraryFunction(IntermediateNode): # This is the continuation of the mypy bug workaround assert self.arbitrary_func is not None return self.arbitrary_func(inputs[0], *self.op_args, **self.op_kwargs) + + def is_equivalent_to(self, other: object) -> bool: + # FIXME: comparing self.arbitrary_func to other.arbitrary_func will not work + # Only evaluating over the same set of inputs and comparing will help + return ( + isinstance(other, ArbitraryFunction) + and self.op_args == other.op_args + and self.op_kwargs == other.op_kwargs + and super().is_equivalent_to(other) + ) diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 188f30e16..5d20cfb81 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -1,7 +1,7 @@ """This file holds the code that can be shared between tracers""" from abc import ABC -from typing import Any, Dict, List, Optional, Tuple, Type, Union +from typing import List, Tuple, Type, Union from ..data_types import BaseValue from ..data_types.scalars import Scalars @@ -29,8 +29,6 @@ class BaseTracer(ABC): self, inputs: List[Union["BaseTracer", Scalars]], computation_to_trace: Type[ir.IntermediateNode], - op_args: Optional[Tuple[Any, ...]] = None, - op_kwargs: Optional[Dict[str, Any]] = None, ) -> Tuple["BaseTracer", ...]: """Helper functions to instantiate all output BaseTracer for a given computation @@ -38,9 +36,6 @@ class BaseTracer(ABC): inputs (List[BaseTracer]): Previous BaseTracer used as inputs for a new node computation_to_trace (Type[ir.IntermediateNode]): The IntermediateNode class to instantiate for the computation being traced - op_args: *args coming from the call being traced - op_kwargs: **kwargs coming from the call being traced - Returns: Tuple[BaseTracer, ...]: A tuple containing an BaseTracer per output function @@ -56,8 +51,6 @@ class BaseTracer(ABC): traced_computation = computation_to_trace( (x.output for x in sanitized_inputs), - op_args=op_args, - op_kwargs=op_kwargs, ) output_tracers = tuple( diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 530742be2..86c94db65 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -81,3 +81,122 @@ def test_evaluate( ): """Test evaluate methods on IntermediateNodes""" assert node.evaluate(input_data) == expected_result + + +@pytest.mark.parametrize( + "node1,node2,expected_result", + [ + ( + ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + True, + ), + ( + ir.Add([EncryptedValue(Integer(16, False)), EncryptedValue(Integer(32, False))]), + ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), + True, + ), + ( + ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + False, + ), + ( + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + True, + ), + ( + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), + True, + ), + ( + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), + ir.Sub([EncryptedValue(Integer(16, False)), EncryptedValue(Integer(32, False))]), + False, + ), + ( + ir.Mul([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Mul([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + True, + ), + ( + ir.Mul([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + False, + ), + ( + ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + False, + ), + ( + ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + True, + ), + ( + ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + ir.Input(EncryptedValue(Integer(32, False)), "y", 0), + False, + ), + ( + ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + ir.Input(EncryptedValue(Integer(32, False)), "x", 1), + False, + ), + ( + ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + ir.Input(EncryptedValue(Integer(8, False)), "x", 0), + False, + ), + ( + ir.ConstantInput(10), + ir.ConstantInput(10), + True, + ), + ( + ir.ConstantInput(10), + ir.Input(EncryptedValue(Integer(8, False)), "x", 0), + False, + ), + ( + ir.ConstantInput(10), + ir.ConstantInput(10.0), + False, + ), + ( + ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + True, + ), + ( + ir.ArbitraryFunction( + EncryptedValue(Integer(8, False)), + lambda x: x, + Integer(8, False), + op_args=(1, 2, 3), + ), + ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + False, + ), + ( + ir.ArbitraryFunction( + EncryptedValue(Integer(8, False)), + lambda x: x, + Integer(8, False), + op_kwargs={"tuple": (1, 2, 3)}, + ), + ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + False, + ), + ], +) +def test_is_equivalent_to( + node1: ir.IntermediateNode, + node2: ir.IntermediateNode, + expected_result: bool, +): + """Test is_equivalent_to methods on IntermediateNodes""" + assert node1.is_equivalent_to(node2) == node2.is_equivalent_to(node1) == expected_result From 3f53c3320e37105bb2dd28dc4b43d5a07374826e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 9 Aug 2021 13:56:38 +0200 Subject: [PATCH 0067/1104] chore(req): add numpy dependency - also add numpy typing plugin to mypy --- mypy.ini | 2 ++ poetry.lock | 2 +- pyproject.toml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..3dc875f9d --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +plugins = numpy.typing.mypy_plugin diff --git a/poetry.lock b/poetry.lock index 378649cef..dbe18bb63 100644 --- a/poetry.lock +++ b/poetry.lock @@ -797,7 +797,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "11d441f0876a8ae3823fef994d552c7982d0122568acbfad3d6a0425b10260f0" +content-hash = "5e466fc94c468da8f58c9b948206b3589f2253c54f0f3f14520a0f08334fe730" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index fc7ea3f50..ffb81ebd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ sphinx-rtd-theme = "^0.5.2" myst-parser = "^0.15.1" networkx = "^2.6.1" matplotlib = "^3.4.2" +numpy = "^1.21.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" From 9ef2154d51ce1da5c29e8e746025f03c1222a9b9 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 9 Aug 2021 16:40:34 +0200 Subject: [PATCH 0068/1104] dev(numpy-dtype): add a function to translate numpy types to frontend types --- hdk/hnumpy/np_dtypes_helpers.py | 49 ++++++++++++++++++++++++++ tests/hnumpy/test_np_dtypes_helpers.py | 31 ++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 hdk/hnumpy/np_dtypes_helpers.py create mode 100644 tests/hnumpy/test_np_dtypes_helpers.py diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py new file mode 100644 index 000000000..a926b188e --- /dev/null +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -0,0 +1,49 @@ +"""File to hold code to manage package and numpy dtypes""" + +from copy import deepcopy + +import numpy +from numpy.typing import DTypeLike + +from ..common.data_types.base import BaseDataType +from ..common.data_types.floats import Float +from ..common.data_types.integers import Integer + +NUMPY_TO_HDK_TYPE_MAPPING = { + numpy.dtype(numpy.int32): Integer(32, is_signed=True), + numpy.dtype(numpy.int64): Integer(64, is_signed=True), + numpy.dtype(numpy.uint32): Integer(32, is_signed=False), + numpy.dtype(numpy.uint64): Integer(64, is_signed=False), + numpy.dtype(numpy.float32): Float(32), + numpy.dtype(numpy.float64): Float(64), +} + +SUPPORTED_NUMPY_TYPES_SET = NUMPY_TO_HDK_TYPE_MAPPING.keys() + + +def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: + """Helper function to get the corresponding type from a numpy dtype + + Args: + numpy_dtype (DTypeLike): Any python object that can be translated to a numpy.dtype + + Raises: + ValueError: If the numpy_dtype is not supported + + Returns: + BaseDataType: The corresponding data type corresponding to the input numpy_dtype + """ + + # Normalize numpy_dtype + normalized_numpy_dtype = numpy.dtype(numpy_dtype) + corresponding_hdk_dtype = NUMPY_TO_HDK_TYPE_MAPPING.get(normalized_numpy_dtype, None) + + if corresponding_hdk_dtype is None: + raise ValueError( + f"Unsupported numpy type: {numpy_dtype} ({normalized_numpy_dtype}), " + f"supported numpy types: " + f"{', '.join(sorted(str(dtype) for dtype in SUPPORTED_NUMPY_TYPES_SET))}" + ) + + # deepcopy to avoid having the value from the dict modified + return deepcopy(corresponding_hdk_dtype) diff --git a/tests/hnumpy/test_np_dtypes_helpers.py b/tests/hnumpy/test_np_dtypes_helpers.py new file mode 100644 index 000000000..ab2fd4201 --- /dev/null +++ b/tests/hnumpy/test_np_dtypes_helpers.py @@ -0,0 +1,31 @@ +"""Test file for hnumpy numpy dtype helpers""" + +import numpy +import pytest + +from hdk.common.data_types.floats import Float +from hdk.common.data_types.integers import Integer +from hdk.hnumpy.np_dtypes_helpers import convert_numpy_dtype_to_common_dtype + + +@pytest.mark.parametrize( + "numpy_dtype,expected_common_type", + [ + pytest.param(numpy.int32, Integer(32, is_signed=True)), + pytest.param("int32", Integer(32, is_signed=True)), + pytest.param(numpy.int64, Integer(64, is_signed=True)), + pytest.param("int64", Integer(64, is_signed=True)), + pytest.param(numpy.uint32, Integer(32, is_signed=False)), + pytest.param("uint32", Integer(32, is_signed=False)), + pytest.param(numpy.uint64, Integer(64, is_signed=False)), + pytest.param("uint64", Integer(64, is_signed=False)), + pytest.param(numpy.float32, Float(32)), + pytest.param("float32", Float(32)), + pytest.param(numpy.float64, Float(64)), + pytest.param("float64", Float(64)), + pytest.param("complex64", None, marks=pytest.mark.xfail(strict=True, raises=ValueError)), + ], +) +def test_convert_numpy_dtype_to_common_dtype(numpy_dtype, expected_common_type): + """Test function for convert_numpy_dtype_to_common_dtype""" + assert convert_numpy_dtype_to_common_dtype(numpy_dtype) == expected_common_type From c51c4bd17a8b4ee91ad6ce471a288879e04e5641 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 9 Aug 2021 17:28:30 +0200 Subject: [PATCH 0069/1104] feat(tracing-astype): add astype method on NPTracer --- hdk/hnumpy/tracing.py | 30 ++++++++++++ tests/hnumpy/test_tracing.py | 92 ++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 8072fdc18..0053e1543 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,14 +1,44 @@ """hnumpy tracing utilities""" from typing import Callable, Dict +import numpy +from numpy.typing import DTypeLike + from ..common.data_types import BaseValue from ..common.operator_graph import OPGraph +from ..common.representation import intermediate as ir from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters +from .np_dtypes_helpers import convert_numpy_dtype_to_common_dtype class NPTracer(BaseTracer): """Tracer class for numpy operations""" + def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": + """Support numpy astype feature, for now it only accepts a dtype and no additional + parameters, *args and **kwargs are accepted for interface compatibility only + + Args: + numpy_dtype (DTypeLike): The object describing a numpy type + + Returns: + NPTracer: The NPTracer representing the casting operation + """ + assert len(args) == 0, f"astype currently only supports tracing without *args, got {args}" + assert ( + len(kwargs) == 0 + ), f"astype currently only supports tracing without **kwargs, got {kwargs}" + + normalized_numpy_dtype = numpy.dtype(numpy_dtype) + output_dtype = convert_numpy_dtype_to_common_dtype(numpy_dtype) + traced_computation = ir.ArbitraryFunction( + input_base_value=self.output, + arbitrary_func=normalized_numpy_dtype.type, + output_dtype=output_dtype, + ) + output_tracer = NPTracer([self], traced_computation=traced_computation, output_index=0) + return output_tracer + def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 4dfada2d6..2634560d2 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -1,8 +1,10 @@ """Test file for hnumpy tracing""" import networkx as nx +import numpy import pytest +from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.representation import intermediate as ir @@ -109,3 +111,93 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): ref_graph.add_edge(input_y, returned_final_node, input_idx=1) assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) + + +@pytest.mark.parametrize( + "function_to_trace,op_graph_expected_output_type,input_and_expected_output_tuples", + [ + ( + lambda x: x.astype(numpy.int32), + Integer(32, is_signed=True), + [ + (14, numpy.int32(14)), + (1.5, numpy.int32(1)), + (2.0, numpy.int32(2)), + (-1.5, numpy.int32(-1)), + (2 ** 31 - 1, numpy.int32(2 ** 31 - 1)), + (-(2 ** 31), numpy.int32(-(2 ** 31))), + ], + ), + ( + lambda x: x.astype(numpy.uint32), + Integer(32, is_signed=False), + [ + (14, numpy.uint32(14)), + (1.5, numpy.uint32(1)), + (2.0, numpy.uint32(2)), + (2 ** 32 - 1, numpy.uint32(2 ** 32 - 1)), + ], + ), + ( + lambda x: x.astype(numpy.int64), + Integer(64, is_signed=True), + [ + (14, numpy.int64(14)), + (1.5, numpy.int64(1)), + (2.0, numpy.int64(2)), + (-1.5, numpy.int64(-1)), + (2 ** 63 - 1, numpy.int64(2 ** 63 - 1)), + (-(2 ** 63), numpy.int64(-(2 ** 63))), + ], + ), + ( + lambda x: x.astype(numpy.uint64), + Integer(64, is_signed=False), + [ + (14, numpy.uint64(14)), + (1.5, numpy.uint64(1)), + (2.0, numpy.uint64(2)), + (2 ** 64 - 1, numpy.uint64(2 ** 64 - 1)), + ], + ), + ( + lambda x: x.astype(numpy.float64), + Float(64), + [ + (14, numpy.float64(14.0)), + (1.5, numpy.float64(1.5)), + (2.0, numpy.float64(2.0)), + (-1.5, numpy.float64(-1.5)), + ], + ), + ( + lambda x: x.astype(numpy.float32), + Float(32), + [ + (14, numpy.float32(14.0)), + (1.5, numpy.float32(1.5)), + (2.0, numpy.float32(2.0)), + (-1.5, numpy.float32(-1.5)), + ], + ), + ], +) +def test_tracing_astype( + function_to_trace, op_graph_expected_output_type, input_and_expected_output_tuples +): + """Test function for NPTracer.astype""" + for input_, expected_output in input_and_expected_output_tuples: + input_value = ( + EncryptedValue(Integer(64, is_signed=True)) + if isinstance(input_, int) + else EncryptedValue(Float(64)) + ) + + op_graph = tracing.trace_numpy_function(function_to_trace, {"x": input_value}) + output_node = op_graph.output_nodes[0] + assert op_graph_expected_output_type == output_node.outputs[0].data_type + + node_results = op_graph.evaluate({0: numpy.array(input_)}) + evaluated_output = node_results[output_node] + assert isinstance(evaluated_output, type(expected_output)) + assert expected_output == evaluated_output From c63ad8ac92e5bcf488e3b863c188dcc473cd5a87 Mon Sep 17 00:00:00 2001 From: Umut Date: Sat, 7 Aug 2021 23:45:12 +0300 Subject: [PATCH 0070/1104] fix(debugging): assign a color to ArbitraryFunction nodes --- hdk/common/debugging/draw_graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index 33cf49bd7..8454dc5f4 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -13,6 +13,7 @@ IR_NODE_COLOR_MAPPING = { ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green", + ir.ArbitraryFunction: "orange", "output": "magenta", } From 56556a85e92d399b9e67a31cf9b165f045640014 Mon Sep 17 00:00:00 2001 From: Umut Date: Sat, 7 Aug 2021 23:49:30 +0300 Subject: [PATCH 0071/1104] feat(representation): create lookup table wrapper to be used during tracing direct table lookups --- docs/dev/GETTING-STARTED.md | 1 + hdk/common/__init__.py | 2 +- hdk/common/common_helpers.py | 15 ++++ hdk/common/extensions/__init__.py | 2 + hdk/common/extensions/table.py | 53 ++++++++++++ tests/common/extensions/test_table.py | 111 ++++++++++++++++++++++++++ tests/common/test_common_helpers.py | 22 ++++- 7 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 hdk/common/extensions/__init__.py create mode 100644 hdk/common/extensions/table.py create mode 100644 tests/common/extensions/test_table.py diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index dc4241d32..f6e4882c0 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -79,6 +79,7 @@ In this section, we will discuss the module structure of hdk briefly. You are en - bounds_measurement: utilities for determining bounds of intermediate representation - data_types: type definitions of typing information of intermediate representation - debugging: utilities for printing/displaying intermediate representation + - extensions: utilities that provide special functionality to our users - representation: type definitions of intermediate representation - tracing: utilities for generic function tracing used during intermediate representation creation - hnumpy: numpy frontend of hdk diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index f498f0d8d..d23b0bb58 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,3 +1,3 @@ """Module for shared data structures and code""" from . import data_types, debugging, representation -from .common_helpers import check_op_graph_is_integer_program +from .common_helpers import check_op_graph_is_integer_program, is_a_power_of_2 diff --git a/hdk/common/common_helpers.py b/hdk/common/common_helpers.py index c0f2d8fdf..efedfcc1d 100644 --- a/hdk/common/common_helpers.py +++ b/hdk/common/common_helpers.py @@ -7,6 +7,21 @@ from .operator_graph import OPGraph from .representation import intermediate as ir +def is_a_power_of_2(x: int) -> bool: + """Check if an integer is a power of two + + Args: + x (int): Number to check + + Returns: + bool: True if the number is a power of two + """ + + # https://stackoverflow.com/questions/57025836/how-to-check-if-a-given-number-is-a-power-of-two + + return x > 0 and (x & (x - 1)) == 0 + + def ir_nodes_has_integer_input_and_output(node: ir.IntermediateNode) -> bool: """Check if an ir node has Integer inputs and outputs diff --git a/hdk/common/extensions/__init__.py b/hdk/common/extensions/__init__.py new file mode 100644 index 000000000..c86a0bf79 --- /dev/null +++ b/hdk/common/extensions/__init__.py @@ -0,0 +1,2 @@ +"""Extensions module to provide additional functionality to our users""" +from . import table diff --git a/hdk/common/extensions/table.py b/hdk/common/extensions/table.py new file mode 100644 index 000000000..d4e362e85 --- /dev/null +++ b/hdk/common/extensions/table.py @@ -0,0 +1,53 @@ +"""This file contains a wrapper class for direct table lookups""" + +from copy import deepcopy +from typing import Iterable, Tuple, Union + +from ..common_helpers import is_a_power_of_2 +from ..data_types.base import BaseDataType +from ..data_types.integers import make_integer_to_hold_ints +from ..representation import intermediate as ir +from ..tracing.base_tracer import BaseTracer + + +class LookupTable: + """Class representing a lookup table""" + + # lookup table itself, has 2^N entries + table: Tuple[int, ...] + + # type of the result of the lookup + output_dtype: BaseDataType + + def __init__(self, table: Iterable[int]): + table = tuple(table) + + if not is_a_power_of_2(len(table)): + raise ValueError( + f"Desired lookup table has inappropriate number of entries ({len(table)})" + ) + + self.table = table + self.output_dtype = make_integer_to_hold_ints(table, force_signed=False) + + def __getitem__(self, item: Union[int, BaseTracer]): + # if a tracer is used for indexing, + # we need to create an `ArbitraryFunction` node + # because the result will be determined during the runtime + if isinstance(item, BaseTracer): + traced_computation = ir.ArbitraryFunction( + input_base_value=item.output, + arbitrary_func=lambda x, table: table[x], + output_dtype=self.output_dtype, + op_kwargs={"table": deepcopy(self.table)}, + ) + return item.__class__( + inputs=[item], + traced_computation=traced_computation, + output_index=0, + ) + + # if not, it means table is indexed with a constant + # thus, the result of the lookup is a constant + # so, we can propagate it directly + return self.table[item] diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py new file mode 100644 index 000000000..530263871 --- /dev/null +++ b/tests/common/extensions/test_table.py @@ -0,0 +1,111 @@ +"""Test file for direct table lookups""" + +from copy import deepcopy + +import networkx as nx +import pytest + +from hdk.common import is_a_power_of_2 +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.common.extensions.table import LookupTable +from hdk.common.representation import intermediate as ir +from hdk.hnumpy import tracing + + +def test_lookup_table_size_constraints(): + """Test function to make sure lookup tables have correct size""" + + table = [] + + # creating empty lookup table is not acceptable + with pytest.raises(ValueError): + LookupTable(table) + + for _ in range(512): + table.append(0) + + if is_a_power_of_2(len(table)): + # creating lookup table with 2^N entries are acceptable + LookupTable(table) + else: + # creating lookup table with anything other than 2^N entries are not acceptable + with pytest.raises(ValueError): + LookupTable(table) + + +def test_lookup_table_encrypted_lookup(test_helpers): + """Test function for tracing with explicit table lookups using encrypted inputs""" + + table = LookupTable([3, 6, 0, 2]) + + def f(x): + return table[x] + + x = EncryptedValue(Integer(2, is_signed=False)) + op_graph = tracing.trace_numpy_function(f, {"x": x}) + + ref_graph = nx.MultiDiGraph() + # Here is the ASCII drawing of the expected graph: + # (x) - (TLU) + + input_x = ir.Input(input_value=x, input_name="x", program_input_idx=0) + ref_graph.add_node(input_x, content=input_x) + + output_arbitrary_function = ir.ArbitraryFunction( + input_base_value=x, + arbitrary_func=lambda x, table: table[x], + output_dtype=table.output_dtype, + op_kwargs={"table": deepcopy(table.table)}, + ) + ref_graph.add_node(output_arbitrary_function, content=output_arbitrary_function) + + ref_graph.add_edge(input_x, output_arbitrary_function, input_idx=0) + + # TODO: discuss if this check is enough as == is not overloaded properly for ArbitraryFunction + assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) + + +def test_lookup_table_encrypted_and_plain_lookup(test_helpers): + """Test function for tracing with explicit table lookups using encrypted and plain inputs""" + + table = LookupTable([3, 6, 0, 2, 1, 4, 5, 7]) + + def f(x): + return table[x] + table[0] + + x = EncryptedValue(Integer(3, is_signed=False)) + op_graph = tracing.trace_numpy_function(f, {"x": x}) + + ref_graph = nx.MultiDiGraph() + # Here is the ASCII drawing of the expected graph: + # (x) - (TLU) + # \ + # (+) + # / + # (3) + + input_x = ir.Input(input_value=x, input_name="x", program_input_idx=0) + ref_graph.add_node(input_x, content=input_x) + + intermediate_arbitrary_function = ir.ArbitraryFunction( + input_base_value=x, + arbitrary_func=lambda x, table: table[x], + output_dtype=table.output_dtype, + op_kwargs={"table": deepcopy(table.table)}, + ) + ref_graph.add_node(intermediate_arbitrary_function, content=intermediate_arbitrary_function) + + constant_3 = ir.ConstantInput(3) + ref_graph.add_node(constant_3, content=constant_3) + + output_add = ir.Add((intermediate_arbitrary_function.outputs[0], constant_3.outputs[0])) + ref_graph.add_node(output_add, content=output_add) + + ref_graph.add_edge(input_x, intermediate_arbitrary_function, input_idx=0) + + ref_graph.add_edge(intermediate_arbitrary_function, output_add, input_idx=0) + ref_graph.add_edge(constant_3, output_add, input_idx=1) + + # TODO: discuss if this check is enough as == is not overloaded properly for ArbitraryFunction + assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index fdbee8538..c0c2aef0c 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -2,13 +2,33 @@ from copy import deepcopy -from hdk.common import check_op_graph_is_integer_program +import pytest + +from hdk.common import check_op_graph_is_integer_program, is_a_power_of_2 from hdk.common.data_types.base import BaseDataType from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import EncryptedValue from hdk.hnumpy.tracing import trace_numpy_function +@pytest.mark.parametrize( + "x,result", + [ + (0, False), + (1, True), + (2, True), + (3, False), + (4, True), + (10, False), + (16, True), + ], +) +def test_is_a_power_of_2(x, result): + """Test function for test_is_a_power_of_2""" + + assert is_a_power_of_2(x) == result + + class DummyNotInteger(BaseDataType): """Dummy helper data type class""" From fe9ab2d21d5ae9b80ab47c12cee22be5a46a3742 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 10 Aug 2021 15:37:46 +0300 Subject: [PATCH 0072/1104] feat(compilation): create compilation artifacts and provide a way to export them in a textual format --- docs/dev/GETTING-STARTED.md | 1 + hdk/common/__init__.py | 2 +- hdk/common/compilation/__init__.py | 3 + hdk/common/compilation/artifacts.py | 83 ++++++++++++++++++++++ hdk/hnumpy/compile.py | 11 ++- tests/common/compilation/test_artifacts.py | 36 ++++++++++ 6 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 hdk/common/compilation/__init__.py create mode 100644 hdk/common/compilation/artifacts.py create mode 100644 tests/common/compilation/test_artifacts.py diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index f6e4882c0..c3e0e6e59 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -77,6 +77,7 @@ In this section, we will discuss the module structure of hdk briefly. You are en - hdk - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) - bounds_measurement: utilities for determining bounds of intermediate representation + - compilation: type definitions related to compilation (e.g., compilation config, compilation artifacts) - data_types: type definitions of typing information of intermediate representation - debugging: utilities for printing/displaying intermediate representation - extensions: utilities that provide special functionality to our users diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index d23b0bb58..49db670e5 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,3 +1,3 @@ """Module for shared data structures and code""" -from . import data_types, debugging, representation +from . import compilation, data_types, debugging, representation from .common_helpers import check_op_graph_is_integer_program, is_a_power_of_2 diff --git a/hdk/common/compilation/__init__.py b/hdk/common/compilation/__init__.py new file mode 100644 index 000000000..2de494630 --- /dev/null +++ b/hdk/common/compilation/__init__.py @@ -0,0 +1,3 @@ +"""Module for compilation related types""" + +from .artifacts import CompilationArtifacts diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py new file mode 100644 index 000000000..02a9dea75 --- /dev/null +++ b/hdk/common/compilation/artifacts.py @@ -0,0 +1,83 @@ +"""Module for compilation artifacts""" + +import platform +import subprocess +from pathlib import Path +from typing import Any, Dict, Optional + +import networkx as nx + +from ..debugging.draw_graph import get_printable_graph +from ..operator_graph import OPGraph +from ..representation import intermediate as ir + + +class CompilationArtifacts: + """Class that conveys information about compilation process""" + + operation_graph: Optional[OPGraph] + bounds: Optional[Dict[ir.IntermediateNode, Dict[str, Any]]] + + def __init__(self): + self.operation_graph = None + self.bounds = None + + def export(self, output_directory: Path): + """Exports the artifacts in a textual format + + Args: + output_directory (Path): the directory to save the artifacts + + Returns: + None + """ + + with open(output_directory.joinpath("environment.txt"), "w") as f: + f.write(f"{platform.platform()} {platform.version()}\n") + f.write(f"Python {platform.python_version()}\n") + + with open(output_directory.joinpath("requirements.txt"), "w") as f: + # example `pip list` output + + # Package Version + # ----------------------------- --------- + # alabaster 0.7.12 + # appdirs 1.4.4 + # ... ... + # ... ... + # wrapt 1.12.1 + # zipp 3.5.0 + + pip_process = subprocess.run(["pip", "list"], stdout=subprocess.PIPE, check=True) + dependencies = iter(pip_process.stdout.decode("utf-8").split("\n")) + + # skip 'Package ... Version' line + next(dependencies) + + # skip '------- ... -------' line + next(dependencies) + + for dependency in dependencies: + tokens = [token for token in dependency.split(" ") if token != ""] + if len(tokens) == 0: + continue + + name = tokens[0] + version = tokens[1] + + f.write(f"{name}=={version}\n") + + if self.operation_graph is not None: + with open(output_directory.joinpath("graph.txt"), "w") as f: + f.write(f"{get_printable_graph(self.operation_graph)[1:]}\n") + + if self.bounds is not None: + with open(output_directory.joinpath("bounds.txt"), "w") as f: + # TODO: + # if nx.topological_sort is not deterministic between calls, + # the lines below will not work properly + # thus, we may want to change this in the future + for index, node in enumerate(nx.topological_sort(self.operation_graph.graph)): + bounds = self.bounds.get(node) + assert bounds is not None + f.write(f"%{index} :: [{bounds.get('min')}, {bounds.get('max')}]\n") diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 0882aa6bd..8662b46f9 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -1,10 +1,11 @@ """hnumpy compilation function""" -from typing import Any, Callable, Dict, Iterator, Tuple +from typing import Any, Callable, Dict, Iterator, Optional, Tuple from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from hdk.hnumpy.tracing import trace_numpy_function +from ..common.compilation import CompilationArtifacts from ..common.data_types import BaseValue from ..common.operator_graph import OPGraph from ..hnumpy.tracing import trace_numpy_function @@ -14,6 +15,7 @@ def compile_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue], dataset: Iterator[Tuple[Any, ...]], + compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: """Main API of hnumpy, to be able to compile an homomorphic program @@ -24,6 +26,8 @@ def compile_numpy_function( dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters + compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill + during compilation Returns: OPGraph: currently returns a compilable graph, but later, it will return an MLIR compatible @@ -39,4 +43,9 @@ def compile_numpy_function( # Update the graph accordingly: after that, we have the compilable graph op_graph.update_values_with_bounds(node_bounds) + # Fill compilation artifacts + if compilation_artifacts is not None: + compilation_artifacts.operation_graph = op_graph + compilation_artifacts.bounds = node_bounds + return op_graph diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py new file mode 100644 index 000000000..5c0f6afc1 --- /dev/null +++ b/tests/common/compilation/test_artifacts.py @@ -0,0 +1,36 @@ +"""Test file for compilation artifacts""" + +import tempfile +from pathlib import Path + +from hdk.common.compilation import CompilationArtifacts +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.hnumpy.compile import compile_numpy_function + + +def test_artifacts_export(): + """Test function to check exporting compilation artifacts""" + + def function(x): + return x + 42 + + artifacts = CompilationArtifacts() + compile_numpy_function( + function, + {"x": EncryptedValue(Integer(7, True))}, + iter([(-2,), (-1,), (0,), (1,), (2,)]), + artifacts, + ) + + with tempfile.TemporaryDirectory() as tmp: + output_directory = Path(tmp) + artifacts.export(output_directory) + + assert output_directory.joinpath("environment.txt").exists() + assert output_directory.joinpath("requirements.txt").exists() + assert output_directory.joinpath("graph.txt").exists() + assert output_directory.joinpath("bounds.txt").exists() + + # format of those files might change in the future + # so it is sufficient to test their existance From e296e9667e0ea50004cce63021dfb7f69465367d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 11 Aug 2021 09:42:31 +0200 Subject: [PATCH 0073/1104] chore(tools): use /bin/bash as shell for the Makefile - tested some commands with an environment which had /bin/sh as default shell, some did not work, this fixes it --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 855351dc8..04557f95f 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SHELL:=/bin/bash + setup_env: poetry install poetry run python -m pip install -U pip wheel setuptools From 19e68589d1da969a7eaa5478b7ae85490687aa46 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 6 Aug 2021 15:04:34 +0200 Subject: [PATCH 0074/1104] dev(numpy-dtypes): add some additional numpy dtype helper functions - add function to convert a type from the project to a numpy dtype - add function to manage numpy ufunc output dtypes - add a check for integers to have positive bit_width - add a check for floats to only accept 32 and 64 bits --- hdk/common/data_types/floats.py | 1 + hdk/common/data_types/integers.py | 1 + hdk/hnumpy/np_dtypes_helpers.py | 91 +++++++++++++++++++++++++- tests/hnumpy/test_np_dtypes_helpers.py | 28 +++++++- 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/hdk/common/data_types/floats.py b/hdk/common/data_types/floats.py index 7021886e0..ed9969680 100644 --- a/hdk/common/data_types/floats.py +++ b/hdk/common/data_types/floats.py @@ -11,6 +11,7 @@ class Float(base.BaseDataType): bit_width: int def __init__(self, bit_width: int) -> None: + assert bit_width in (32, 64), "Only 32 and 64 bits floats are supported" self.bit_width = bit_width def __repr__(self) -> str: diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index f4753f09b..4ebf7e3d2 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -13,6 +13,7 @@ class Integer(base.BaseDataType): is_signed: bool def __init__(self, bit_width: int, is_signed: bool) -> None: + assert bit_width > 0, "bit_width must be > 0" self.bit_width = bit_width self.is_signed = is_signed diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index a926b188e..747a9c9c7 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -1,11 +1,13 @@ """File to hold code to manage package and numpy dtypes""" from copy import deepcopy +from typing import List, Union import numpy from numpy.typing import DTypeLike from ..common.data_types.base import BaseDataType +from ..common.data_types.dtypes_helpers import SUPPORTED_TYPES from ..common.data_types.floats import Float from ..common.data_types.integers import Integer @@ -18,7 +20,9 @@ NUMPY_TO_HDK_TYPE_MAPPING = { numpy.dtype(numpy.float64): Float(64), } -SUPPORTED_NUMPY_TYPES_SET = NUMPY_TO_HDK_TYPE_MAPPING.keys() +SUPPORTED_NUMPY_TYPES_SET = set(NUMPY_TO_HDK_TYPE_MAPPING.keys()) + +SUPPORTED_TYPE_MSG_STRING = ", ".join(sorted(str(dtype) for dtype in SUPPORTED_NUMPY_TYPES_SET)) def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: @@ -42,8 +46,91 @@ def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: raise ValueError( f"Unsupported numpy type: {numpy_dtype} ({normalized_numpy_dtype}), " f"supported numpy types: " - f"{', '.join(sorted(str(dtype) for dtype in SUPPORTED_NUMPY_TYPES_SET))}" + f"{SUPPORTED_TYPE_MSG_STRING}" ) # deepcopy to avoid having the value from the dict modified return deepcopy(corresponding_hdk_dtype) + + +def convert_common_dtype_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dtype: + """Convert a BaseDataType to corresponding numpy.dtype + + Args: + common_dtype (BaseDataType): dtype to convert to numpy.dtype + + Returns: + numpy.dtype: The resulting numpy.dtype + """ + assert isinstance( + common_dtype, SUPPORTED_TYPES + ), f"Unsupported common_dtype: {type(common_dtype)}" + type_to_return: numpy.dtype + + if isinstance(common_dtype, Float): + assert common_dtype.bit_width in ( + 32, + 64, + ), "Only converting Float(32) or Float(64) is supported" + type_to_return = ( + numpy.dtype(numpy.float64) + if common_dtype.bit_width == 64 + else numpy.dtype(numpy.float32) + ) + elif isinstance(common_dtype, Integer): + signed = common_dtype.is_signed + if common_dtype.bit_width <= 32: + type_to_return = numpy.dtype(numpy.int32) if signed else numpy.dtype(numpy.uint32) + elif common_dtype.bit_width <= 64: + type_to_return = numpy.dtype(numpy.int64) if signed else numpy.dtype(numpy.uint64) + else: + raise NotImplementedError( + f"Conversion to numpy dtype only supports Integers with bit_width <= 64, " + f"got {common_dtype!r}" + ) + + return type_to_return + + +def get_ufunc_numpy_output_dtype( + ufunc: numpy.ufunc, + input_dtypes: Union[List[numpy.dtype], List[BaseDataType]], +) -> List[numpy.dtype]: + """Function to record the output dtype of a numpy.ufunc given some input types + + Args: + ufunc (numpy.ufunc): The numpy.ufunc whose output types need to be recorded + input_dtypes (Union[List[numpy.dtype], List[BaseDataType]]): Either numpy or common dtypes + in the same order as they will be used with the ufunc inputs + + Returns: + List[numpy.dtype]: The ordered numpy dtypes of the ufunc outputs + """ + assert ( + len(input_dtypes) == ufunc.nin + ), f"Expected {ufunc.nin} types, got {len(input_dtypes)}: {input_dtypes}" + + input_dtypes = [ + numpy.dtype(convert_common_dtype_to_numpy_dtype(dtype)) + if not isinstance(dtype, numpy.dtype) + else dtype + for dtype in input_dtypes + ] + + # Store numpy old error settings and ignore all errors in this function + # We ignore errors as we may call functions with invalid inputs just to get the proper output + # dtypes + old_numpy_err_settings = numpy.seterr(all="ignore") + + dummy_inputs = tuple( + dtype.type(1000.0 * numpy.random.random_sample()) for dtype in input_dtypes + ) + + outputs = ufunc(*dummy_inputs) + if not isinstance(outputs, tuple): + outputs = (outputs,) + + # Restore numpy error settings + numpy.seterr(**old_numpy_err_settings) + + return [output.dtype for output in outputs] diff --git a/tests/hnumpy/test_np_dtypes_helpers.py b/tests/hnumpy/test_np_dtypes_helpers.py index ab2fd4201..269982390 100644 --- a/tests/hnumpy/test_np_dtypes_helpers.py +++ b/tests/hnumpy/test_np_dtypes_helpers.py @@ -5,7 +5,10 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.hnumpy.np_dtypes_helpers import convert_numpy_dtype_to_common_dtype +from hdk.hnumpy.np_dtypes_helpers import ( + convert_common_dtype_to_numpy_dtype, + convert_numpy_dtype_to_common_dtype, +) @pytest.mark.parametrize( @@ -29,3 +32,26 @@ from hdk.hnumpy.np_dtypes_helpers import convert_numpy_dtype_to_common_dtype def test_convert_numpy_dtype_to_common_dtype(numpy_dtype, expected_common_type): """Test function for convert_numpy_dtype_to_common_dtype""" assert convert_numpy_dtype_to_common_dtype(numpy_dtype) == expected_common_type + + +@pytest.mark.parametrize( + "common_dtype,expected_numpy_dtype", + [ + pytest.param(Integer(7, is_signed=False), numpy.uint32), + pytest.param(Integer(7, is_signed=True), numpy.int32), + pytest.param(Integer(32, is_signed=True), numpy.int32), + pytest.param(Integer(64, is_signed=True), numpy.int64), + pytest.param(Integer(32, is_signed=False), numpy.uint32), + pytest.param(Integer(64, is_signed=False), numpy.uint64), + pytest.param(Float(32), numpy.float32), + pytest.param(Float(64), numpy.float64), + pytest.param( + Integer(128, is_signed=True), + None, + marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), + ), + ], +) +def test_convert_common_dtype_to_numpy_dtype(common_dtype, expected_numpy_dtype): + """Test function for convert_common_dtype_to_numpy_dtype""" + assert expected_numpy_dtype == convert_common_dtype_to_numpy_dtype(common_dtype) From 7bdcfabbfed3be39a63523c2594b4f7cf8e3625b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 10 Aug 2021 17:09:59 +0200 Subject: [PATCH 0075/1104] feat(hnp-tracing): add support for ufunc routing to NPTracer - start tracing numpy.rint and manage dtypes - update BaseTracer to accept iterables as inputs, because NPTracer does not get list givent the way numpy sends arguments to the functions --- hdk/common/tracing/base_tracer.py | 8 ++-- hdk/hnumpy/np_dtypes_helpers.py | 17 +++---- hdk/hnumpy/tracing.py | 79 +++++++++++++++++++++++++++++-- tests/hnumpy/test_tracing.py | 78 ++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 18 deletions(-) diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 5d20cfb81..b3fa03fae 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -1,7 +1,7 @@ """This file holds the code that can be shared between tracers""" from abc import ABC -from typing import List, Tuple, Type, Union +from typing import Iterable, List, Tuple, Type, Union from ..data_types import BaseValue from ..data_types.scalars import Scalars @@ -17,17 +17,17 @@ class BaseTracer(ABC): def __init__( self, - inputs: List["BaseTracer"], + inputs: Iterable["BaseTracer"], traced_computation: ir.IntermediateNode, output_index: int, ) -> None: - self.inputs = inputs + self.inputs = list(inputs) self.traced_computation = traced_computation self.output = traced_computation.outputs[output_index] def instantiate_output_tracers( self, - inputs: List[Union["BaseTracer", Scalars]], + inputs: Iterable[Union["BaseTracer", Scalars]], computation_to_trace: Type[ir.IntermediateNode], ) -> Tuple["BaseTracer", ...]: """Helper functions to instantiate all output BaseTracer for a given computation diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 747a9c9c7..a45333ce3 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -1,7 +1,7 @@ """File to hold code to manage package and numpy dtypes""" from copy import deepcopy -from typing import List, Union +from typing import List import numpy from numpy.typing import DTypeLike @@ -94,14 +94,14 @@ def convert_common_dtype_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dty def get_ufunc_numpy_output_dtype( ufunc: numpy.ufunc, - input_dtypes: Union[List[numpy.dtype], List[BaseDataType]], + input_dtypes: List[BaseDataType], ) -> List[numpy.dtype]: """Function to record the output dtype of a numpy.ufunc given some input types Args: ufunc (numpy.ufunc): The numpy.ufunc whose output types need to be recorded - input_dtypes (Union[List[numpy.dtype], List[BaseDataType]]): Either numpy or common dtypes - in the same order as they will be used with the ufunc inputs + input_dtypes (List[BaseDataType]): Common dtypes in the same order as they will be used with + the ufunc inputs Returns: List[numpy.dtype]: The ordered numpy dtypes of the ufunc outputs @@ -110,12 +110,7 @@ def get_ufunc_numpy_output_dtype( len(input_dtypes) == ufunc.nin ), f"Expected {ufunc.nin} types, got {len(input_dtypes)}: {input_dtypes}" - input_dtypes = [ - numpy.dtype(convert_common_dtype_to_numpy_dtype(dtype)) - if not isinstance(dtype, numpy.dtype) - else dtype - for dtype in input_dtypes - ] + input_numpy_dtypes = [convert_common_dtype_to_numpy_dtype(dtype) for dtype in input_dtypes] # Store numpy old error settings and ignore all errors in this function # We ignore errors as we may call functions with invalid inputs just to get the proper output @@ -123,7 +118,7 @@ def get_ufunc_numpy_output_dtype( old_numpy_err_settings = numpy.seterr(all="ignore") dummy_inputs = tuple( - dtype.type(1000.0 * numpy.random.random_sample()) for dtype in input_dtypes + dtype.type(1000.0 * numpy.random.random_sample()) for dtype in input_numpy_dtypes ) outputs = ufunc(*dummy_inputs) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 0053e1543..912b40d88 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,5 +1,5 @@ """hnumpy tracing utilities""" -from typing import Callable, Dict +from typing import Callable, Dict, Mapping import numpy from numpy.typing import DTypeLike @@ -8,12 +8,28 @@ from ..common.data_types import BaseValue from ..common.operator_graph import OPGraph from ..common.representation import intermediate as ir from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters -from .np_dtypes_helpers import convert_numpy_dtype_to_common_dtype +from .np_dtypes_helpers import ( + convert_numpy_dtype_to_common_dtype, + get_ufunc_numpy_output_dtype, +) class NPTracer(BaseTracer): """Tracer class for numpy operations""" + def __array_ufunc__(self, ufunc, method, *input_tracers, **kwargs): + """ + Catch calls to numpy ufunc and routes them to tracing functions if supported + read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch + """ + if method == "__call__": + tracing_func = self.get_tracing_func_for_np_ufunc(ufunc) + assert ( + len(kwargs) == 0 + ), f"hnumpy does not support **kwargs currently for numpy ufuncs, ufunc: {ufunc}" + return tracing_func(self, *input_tracers, **kwargs) + raise NotImplementedError("Only __call__ method is supported currently") + def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": """Support numpy astype feature, for now it only accepts a dtype and no additional parameters, *args and **kwargs are accepted for interface compatibility only @@ -36,9 +52,66 @@ class NPTracer(BaseTracer): arbitrary_func=normalized_numpy_dtype.type, output_dtype=output_dtype, ) - output_tracer = NPTracer([self], traced_computation=traced_computation, output_index=0) + output_tracer = self.__class__( + [self], traced_computation=traced_computation, output_index=0 + ) return output_tracer + @staticmethod + def get_tracing_func_for_np_ufunc(ufunc: numpy.ufunc) -> Callable: + """Get the tracing function for a numpy ufunc + + Args: + ufunc (numpy.ufunc): The numpy ufunc that will be traced + + Raises: + NotImplementedError: Raised if the passed ufunc is not supported by NPTracer + + Returns: + Callable: the tracing function that needs to be called to trace ufunc + """ + tracing_func = NPTracer.UFUNC_ROUTING.get(ufunc, None) + if tracing_func is None: + raise NotImplementedError( + f"NPTracer does not yet manage the following ufunc: {ufunc.__name__}" + ) + return tracing_func + + @staticmethod + def _manage_dtypes(ufunc: numpy.ufunc, *input_tracers: "NPTracer"): + output_dtypes = get_ufunc_numpy_output_dtype( + ufunc, [input_tracer.output.data_type for input_tracer in input_tracers] + ) + common_output_dtypes = [ + convert_numpy_dtype_to_common_dtype(dtype) for dtype in output_dtypes + ] + return common_output_dtypes + + def rint(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.rint + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + assert len(input_tracers) == 1 + common_output_dtypes = self._manage_dtypes(numpy.rint, *input_tracers) + assert len(common_output_dtypes) == 1 + + traced_computation = ir.ArbitraryFunction( + input_base_value=input_tracers[0].output, + arbitrary_func=numpy.rint, + output_dtype=common_output_dtypes[0], + op_kwargs=kwargs, + ) + output_tracer = self.__class__( + input_tracers, traced_computation=traced_computation, output_index=0 + ) + return output_tracer + + UFUNC_ROUTING: Mapping[numpy.ufunc, Callable] = { + numpy.rint: rint, + } + def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 2634560d2..1649aa2a5 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -201,3 +201,81 @@ def test_tracing_astype( evaluated_output = node_results[output_node] assert isinstance(evaluated_output, type(expected_output)) assert expected_output == evaluated_output + + +@pytest.mark.parametrize( + "function_to_trace,inputs,expected_output_node,expected_output_value", + [ + # We cannot call trace_numpy_function on some numpy function as getting the signature for + # these functions fails, so we wrap it in a lambda + # pylint: disable=unnecessary-lambda + pytest.param( + lambda x: numpy.rint(x), + {"x": EncryptedValue(Integer(7, is_signed=False))}, + ir.ArbitraryFunction, + EncryptedValue(Float(64)), + ), + pytest.param( + lambda x: numpy.rint(x), + {"x": EncryptedValue(Integer(32, is_signed=True))}, + ir.ArbitraryFunction, + EncryptedValue(Float(64)), + ), + pytest.param( + lambda x: numpy.rint(x), + {"x": EncryptedValue(Integer(64, is_signed=True))}, + ir.ArbitraryFunction, + EncryptedValue(Float(64)), + ), + pytest.param( + lambda x: numpy.rint(x), + {"x": EncryptedValue(Integer(128, is_signed=True))}, + ir.ArbitraryFunction, + None, + marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), + ), + pytest.param( + lambda x: numpy.rint(x), + {"x": EncryptedValue(Float(64))}, + ir.ArbitraryFunction, + EncryptedValue(Float(64)), + ), + # The next test case is only for coverage purposes, to trigger the unsupported method + # exception handling + pytest.param( + lambda x: numpy.add.reduce(x), + {"x": EncryptedValue(Integer(32, is_signed=True))}, + None, + None, + marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), + ), + # pylint: enable=unnecessary-lambda + ], +) +def test_trace_hnumpy_supported_ufuncs( + function_to_trace, inputs, expected_output_node, expected_output_value +): + """Function to trace supported numpy ufuncs""" + op_graph = tracing.trace_numpy_function(function_to_trace, inputs) + + assert len(op_graph.output_nodes) == 1 + assert isinstance(op_graph.output_nodes[0], expected_output_node) + assert len(op_graph.output_nodes[0].outputs) == 1 + assert op_graph.output_nodes[0].outputs[0] == expected_output_value + + +@pytest.mark.parametrize( + "np_ufunc,expected_tracing_func", + [ + pytest.param(numpy.rint, tracing.NPTracer.rint), + # There is a need to test the case where the function fails, I chose numpy.conjugate which + # works on complex types, as we don't talk about complex types for now this looks like a + # good long term candidate to check for an unsupported function + pytest.param( + numpy.conjugate, None, marks=pytest.mark.xfail(strict=True, raises=NotImplementedError) + ), + ], +) +def test_nptracer_get_tracing_func_for_np_ufunc(np_ufunc, expected_tracing_func): + """Test NPTracer get_tracing_func_for_np_ufunc""" + assert tracing.NPTracer.get_tracing_func_for_np_ufunc(np_ufunc) == expected_tracing_func From 765df12a2dc9831c0a4ab97b18d7b57004c34e41 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 10 Aug 2021 15:56:37 +0200 Subject: [PATCH 0076/1104] doc: add autogenerated doc in sphinx refs #118 --- .gitignore | 1 + Makefile | 16 ++++++++++++++++ docs/conf.py | 28 +++++++++++++--------------- docs/dev/GETTING-STARTED.md | 11 +++++++++++ docs/index.rst | 6 ++++++ 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 851e2ddea..7ba41e14c 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/_apidoc/ # PyBuilder target/ diff --git a/Makefile b/Makefile index 04557f95f..93fcdaf14 100644 --- a/Makefile +++ b/Makefile @@ -58,5 +58,21 @@ coverage: .PHONY: coverage docs: + + # Generate the auto summary of documentations + poetry run sphinx-apidoc -o docs/_apidoc hdk + + # Docs cd docs && poetry run make html + +clean_docs: + rm -rf docs/_apidoc docs/_build + +open_docs: + open docs/_build/html/index.html + +build_and_open_docs: clean_docs docs open_docs + + + .PHONY: docs diff --git a/docs/conf.py b/docs/conf.py index bc2acb827..f229e37ae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,12 +17,12 @@ # -- Project information ----------------------------------------------------- -project = 'Homomorphic Development Kit' -copyright = '2021, Zama' -author = 'Zama' +project = "Homomorphic Development Kit" +copyright = "2021, Zama" +author = "Zama" # The full version, including alpha/beta/rc tags -release = '0.1' +release = "0.1" # -- General configuration --------------------------------------------------- @@ -30,9 +30,7 @@ release = '0.1' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'myst_parser' -] +extensions = ["myst_parser", "sphinx.ext.autodoc", "sphinx.ext.autosummary"] myst_enable_extensions = [ "amsmath", @@ -40,12 +38,12 @@ myst_enable_extensions = [ ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- @@ -53,16 +51,16 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' -html_style = 'css/zama.css' -html_logo = 'logo-black.png' +html_theme = "sphinx_rtd_theme" +html_style = "css/zama.css" +html_logo = "logo-black.png" html_theme_options = { - 'logo_only': False, - 'display_version': True, + "logo_only": False, + "display_version": True, } pygments_style = "zenburn" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index c3e0e6e59..1c93bbf53 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -167,3 +167,14 @@ make coverage Note that this will compare the coverage with `origin/main`. If you want to set a custom base branch, you can specify `BB` environment variable like so `BB=$YOUR_BASE_BRANCH make coverage`. If your coverage is below hundred percent, you should write more tests and then create the pull request. If you ignore this warning and create the PR, GitHub actions will fail and your PR will not be merged anyway. + +### Making docs with Sphinx + +One can simply create docs with Sphinx and open them, by doing: + +```shell +make build_and_open_docs +``` + +The documentation contains both files written by hand by developpers and files automatically created by parsing the source files. + diff --git a/docs/index.rst b/docs/index.rst index 51da0dcdb..abeb8fee5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,3 +8,9 @@ Homomorphic Development Kit's documentation dev/ARCHITECTURE.md dev/GETTING-STARTED.md + +.. toctree:: + :maxdepth: 5 + :caption: Docs from sources + + _apidoc/hdk.rst From 8f3e461e3b16f954b08a1bd774cde32da2ae5973 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 11 Aug 2021 12:02:42 +0200 Subject: [PATCH 0077/1104] fix: docstring following google conventions refs #122 --- Makefile | 19 ++++++++----- docs/conf.py | 2 +- docs/index.rst | 2 +- hdk/__init__.py | 2 +- hdk/common/__init__.py | 2 +- hdk/common/bounds_measurement/__init__.py | 2 +- hdk/common/bounds_measurement/dataset_eval.py | 6 ++-- hdk/common/common_helpers.py | 9 +++--- hdk/common/compilation/__init__.py | 2 +- hdk/common/compilation/artifacts.py | 7 ++--- hdk/common/data_types/__init__.py | 2 +- hdk/common/data_types/base.py | 4 +-- hdk/common/data_types/dtypes_helpers.py | 21 +++++++------- hdk/common/data_types/floats.py | 4 +-- hdk/common/data_types/integers.py | 20 ++++++------- hdk/common/data_types/scalars.py | 2 +- hdk/common/data_types/values.py | 8 +++--- hdk/common/debugging/__init__.py | 2 +- hdk/common/debugging/draw_graph.py | 15 ++++------ hdk/common/extensions/__init__.py | 2 +- hdk/common/extensions/table.py | 4 +-- hdk/common/operator_graph.py | 13 +++++---- hdk/common/representation/__init__.py | 2 +- hdk/common/representation/intermediate.py | 28 +++++++++++-------- hdk/common/tracing/__init__.py | 2 +- hdk/common/tracing/base_tracer.py | 12 +++----- hdk/common/tracing/tracing_helpers.py | 10 +++---- hdk/hnumpy/__init__.py | 2 +- hdk/hnumpy/compile.py | 5 ++-- hdk/hnumpy/np_dtypes_helpers.py | 9 +++--- hdk/hnumpy/tracing.py | 22 ++++++++------- poetry.lock | 25 ++++++++++++++++- pyproject.toml | 1 + 33 files changed, 147 insertions(+), 121 deletions(-) diff --git a/Makefile b/Makefile index 93fcdaf14..3e5e4fe46 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ pylint: conformance: python_format .PHONY: conformance -pcc: check_python_format pylint mypy_ci +pcc: check_python_format pylint mypy_ci pydocstyle .PHONY: pcc pytest: @@ -58,21 +58,26 @@ coverage: .PHONY: coverage docs: - - # Generate the auto summary of documentations + @# Generate the auto summary of documentations poetry run sphinx-apidoc -o docs/_apidoc hdk - # Docs + @# Docs cd docs && poetry run make html +.PHONY: docs clean_docs: rm -rf docs/_apidoc docs/_build +.PHONY: clean_docs open_docs: + @# This is macOS only. On other systems, one would use `start` or `xdg-open` open docs/_build/html/index.html +.PHONY: open_docs build_and_open_docs: clean_docs docs open_docs +.PHONY: build_and_open_docs - - -.PHONY: docs +pydocstyle: + @# From http://www.pydocstyle.org/en/stable/error_codes.html + poetry run pydocstyle hdk --convention google --add-ignore=D1,D202 +.PHONY: pydocstyle diff --git a/docs/conf.py b/docs/conf.py index f229e37ae..fef124892 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ release = "0.1" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["myst_parser", "sphinx.ext.autodoc", "sphinx.ext.autosummary"] +extensions = ["sphinx.ext.napoleon", "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.autosummary"] myst_enable_extensions = [ "amsmath", diff --git a/docs/index.rst b/docs/index.rst index abeb8fee5..0e4c71a20 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,4 +13,4 @@ Homomorphic Development Kit's documentation :maxdepth: 5 :caption: Docs from sources - _apidoc/hdk.rst + _apidoc/modules.rst diff --git a/hdk/__init__.py b/hdk/__init__.py index 7189e5b42..983f90088 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -1,2 +1,2 @@ -"""Package top import""" +"""Package top import.""" from . import common, hnumpy diff --git a/hdk/common/__init__.py b/hdk/common/__init__.py index 49db670e5..0dcf40159 100644 --- a/hdk/common/__init__.py +++ b/hdk/common/__init__.py @@ -1,3 +1,3 @@ -"""Module for shared data structures and code""" +"""Module for shared data structures and code.""" from . import compilation, data_types, debugging, representation from .common_helpers import check_op_graph_is_integer_program, is_a_power_of_2 diff --git a/hdk/common/bounds_measurement/__init__.py b/hdk/common/bounds_measurement/__init__.py index 00836be57..9bd6c5c7a 100644 --- a/hdk/common/bounds_measurement/__init__.py +++ b/hdk/common/bounds_measurement/__init__.py @@ -1,2 +1,2 @@ -"""Bounds measurement module""" +"""Bounds measurement module.""" from . import dataset_eval diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/hdk/common/bounds_measurement/dataset_eval.py index 8fb8d5df9..d3684ce6c 100644 --- a/hdk/common/bounds_measurement/dataset_eval.py +++ b/hdk/common/bounds_measurement/dataset_eval.py @@ -1,4 +1,4 @@ -"""Code to evaluate the IR graph on datasets""" +"""Code to evaluate the IR graph on datasets.""" from typing import Any, Iterator, Tuple @@ -6,7 +6,9 @@ from ..operator_graph import OPGraph def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, dataset: Iterator[Tuple[Any, ...]]): - """Evaluate the bounds for all output values of the operators in the graph op_graph over data + """Evaluate the bounds with a dataset. + + Evaluate the bounds for all output values of the operators in the graph op_graph over data coming from the dataset Args: diff --git a/hdk/common/common_helpers.py b/hdk/common/common_helpers.py index efedfcc1d..71dac78be 100644 --- a/hdk/common/common_helpers.py +++ b/hdk/common/common_helpers.py @@ -1,4 +1,4 @@ -"""File to hold some helper code""" +"""File to hold some helper code.""" from typing import List, Optional @@ -8,7 +8,7 @@ from .representation import intermediate as ir def is_a_power_of_2(x: int) -> bool: - """Check if an integer is a power of two + """Check if an integer is a power of two. Args: x (int): Number to check @@ -16,14 +16,13 @@ def is_a_power_of_2(x: int) -> bool: Returns: bool: True if the number is a power of two """ - # https://stackoverflow.com/questions/57025836/how-to-check-if-a-given-number-is-a-power-of-two return x > 0 and (x & (x - 1)) == 0 def ir_nodes_has_integer_input_and_output(node: ir.IntermediateNode) -> bool: - """Check if an ir node has Integer inputs and outputs + """Check if an ir node has Integer inputs and outputs. Args: node (ir.IntermediateNode): Node to check @@ -42,7 +41,7 @@ def check_op_graph_is_integer_program( op_graph: OPGraph, offending_nodes_out: Optional[List[ir.IntermediateNode]] = None, ) -> bool: - """Check if an op_graph inputs, outputs and intermediate values are Integers + """Check if an op_graph inputs, outputs and intermediate values are Integers. Args: op_graph (OPGraph): The OPGraph to check diff --git a/hdk/common/compilation/__init__.py b/hdk/common/compilation/__init__.py index 2de494630..3dba32973 100644 --- a/hdk/common/compilation/__init__.py +++ b/hdk/common/compilation/__init__.py @@ -1,3 +1,3 @@ -"""Module for compilation related types""" +"""Module for compilation related types.""" from .artifacts import CompilationArtifacts diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index 02a9dea75..5332d9772 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -1,4 +1,4 @@ -"""Module for compilation artifacts""" +"""Module for compilation artifacts.""" import platform import subprocess @@ -13,7 +13,7 @@ from ..representation import intermediate as ir class CompilationArtifacts: - """Class that conveys information about compilation process""" + """Class that conveys information about compilation process.""" operation_graph: Optional[OPGraph] bounds: Optional[Dict[ir.IntermediateNode, Dict[str, Any]]] @@ -23,7 +23,7 @@ class CompilationArtifacts: self.bounds = None def export(self, output_directory: Path): - """Exports the artifacts in a textual format + """Exports the artifacts in a textual format. Args: output_directory (Path): the directory to save the artifacts @@ -31,7 +31,6 @@ class CompilationArtifacts: Returns: None """ - with open(output_directory.joinpath("environment.txt"), "w") as f: f.write(f"{platform.platform()} {platform.version()}\n") f.write(f"Python {platform.python_version()}\n") diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index 54bbbd739..fb67c003b 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,3 +1,3 @@ -"""Module for data types code and data structures""" +"""Module for data types code and data structures.""" from . import dtypes_helpers, integers, values from .values import BaseValue diff --git a/hdk/common/data_types/base.py b/hdk/common/data_types/base.py index 47090800d..13ef63fe8 100644 --- a/hdk/common/data_types/base.py +++ b/hdk/common/data_types/base.py @@ -1,7 +1,7 @@ -"""File holding code to represent data types in a program""" +"""File holding code to represent data types in a program.""" from abc import ABC class BaseDataType(ABC): - """Base class to represent a data type""" + """Base class to represent a data type.""" diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 1c93737e6..310fcd6bd 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -1,4 +1,4 @@ -"""File to hold helper functions for data types related stuff""" +"""File to hold helper functions for data types related stuff.""" from copy import deepcopy from typing import cast @@ -14,7 +14,7 @@ SUPPORTED_TYPES = INTEGER_TYPES + FLOAT_TYPES def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted_integer + """Helper function to check that a value is an encrypted_integer. Args: value_to_check (BaseValue): The value to check @@ -28,7 +28,7 @@ def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: def value_is_encrypted_unsigned_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted_integer + """Helper function to check that a value is an encrypted_integer. Args: value_to_check (BaseValue): The value to check @@ -36,7 +36,6 @@ def value_is_encrypted_unsigned_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is an encrypted value of type Integer """ - return ( value_is_encrypted_integer(value_to_check) and not cast(Integer, value_to_check.data_type).is_signed @@ -47,8 +46,9 @@ def find_type_to_hold_both_lossy( dtype1: BaseDataType, dtype2: BaseDataType, ) -> BaseDataType: - """Determine the type that can represent both dtype1 and dtype2 separately, this is lossy with - floating point types + """Determine the type that can represent both dtype1 and dtype2 separately. + + This is lossy with floating point types. Args: dtype1 (BaseDataType): first dtype to hold @@ -102,9 +102,11 @@ def find_type_to_hold_both_lossy( def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> BaseValue: - """Returns a Value that would result from computation on both value1 and value2 while - determining the data type able to hold both value1 and value2 data type (this can be lossy - with floats) + """Return mixed value with data type able to hold both value1 and value2 dtypes. + + Returns a Value that would result from computation on both value1 and value2 while + determining the data type able to hold both value1 and value2 data type (this can be lossy + with floats) Args: value1 (BaseValue): first value to mix @@ -114,7 +116,6 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> BaseValue: The resulting mixed value with data type able to hold both value1 and value2 dtypes """ - holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) mixed_value: BaseValue diff --git a/hdk/common/data_types/floats.py b/hdk/common/data_types/floats.py index ed9969680..d9dcd7128 100644 --- a/hdk/common/data_types/floats.py +++ b/hdk/common/data_types/floats.py @@ -1,10 +1,10 @@ -"""This file holds the definitions for floating point types""" +"""This file holds the definitions for floating point types.""" from . import base class Float(base.BaseDataType): - """Class representing a float""" + """Class representing a float.""" # bit_width is the total number of bits used to represent a floating point number, including # sign bit, exponent and mantissa diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index 4ebf7e3d2..dab19380a 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -1,4 +1,4 @@ -"""This file holds the definitions for integer types""" +"""This file holds the definitions for integer types.""" import math from typing import Iterable @@ -7,7 +7,7 @@ from . import base class Integer(base.BaseDataType): - """Class representing an integer""" + """Class representing an integer.""" bit_width: int is_signed: bool @@ -29,21 +29,21 @@ class Integer(base.BaseDataType): ) def min_value(self) -> int: - """Minimum value representable by the Integer""" + """Minimum value representable by the Integer.""" if self.is_signed: return -(2 ** (self.bit_width - 1)) return 0 def max_value(self) -> int: - """Maximum value representable by the Integer""" + """Maximum value representable by the Integer.""" if self.is_signed: return 2 ** (self.bit_width - 1) - 1 return 2 ** self.bit_width - 1 def can_represent_value(self, value_to_represent: int) -> bool: - """A helper function to check if a value is representable by the Integer + """A helper function to check if a value is representable by the Integer. Args: value_to_represent (int): Value to check @@ -55,7 +55,7 @@ class Integer(base.BaseDataType): def create_signed_integer(bit_width: int) -> Integer: - """Convenience function to create a signed integer + """Convenience function to create a signed integer. Args: bit_width (int): width of the integer @@ -70,7 +70,7 @@ SignedInteger = create_signed_integer def create_unsigned_integer(bit_width: int) -> Integer: - """Convenience function to create an unsigned integer + """Convenience function to create an unsigned integer. Args: bit_width (int): width of the integer @@ -78,7 +78,6 @@ def create_unsigned_integer(bit_width: int) -> Integer: Returns: Integer: An unsigned integer with the requested bit_width """ - return Integer(bit_width, is_signed=False) @@ -86,7 +85,7 @@ UnsignedInteger = create_unsigned_integer def make_integer_to_hold_ints(values: Iterable[int], force_signed: bool) -> Integer: - """Returns an Integer able to hold all values, it is possible to force the Integer to be signed + """Returns an Integer able to hold all values, it is possible to force the Integer to be signed. Args: values (Iterable[int]): The values to hold @@ -110,7 +109,7 @@ def make_integer_to_hold_ints(values: Iterable[int], force_signed: bool) -> Inte def get_bits_to_represent_int(value: int, force_signed: bool) -> int: - """Returns how many bits are required to represent a single int + """Returns how many bits are required to represent a single int. Args: value (int): The int for which we want to know how many bits are required @@ -119,7 +118,6 @@ def get_bits_to_represent_int(value: int, force_signed: bool) -> int: Returns: int: required amount of bits """ - # Writing this in a very dumb way num_bits: int if value < 0: diff --git a/hdk/common/data_types/scalars.py b/hdk/common/data_types/scalars.py index 968d0b7fb..078777cb5 100644 --- a/hdk/common/data_types/scalars.py +++ b/hdk/common/data_types/scalars.py @@ -1,4 +1,4 @@ -"""File holding code to represent data types used for constants in programs""" +"""File holding code to represent data types used for constants in programs.""" from typing import Union diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index 9ca75d249..21c6de931 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -1,4 +1,4 @@ -"""File holding classes representing values used by an FHE program""" +"""File holding classes representing values used by an FHE program.""" from abc import ABC @@ -6,7 +6,7 @@ from . import base class BaseValue(ABC): - """Abstract base class to represent any kind of value in a program""" + """Abstract base class to represent any kind of value in a program.""" data_type: base.BaseDataType @@ -21,8 +21,8 @@ class BaseValue(ABC): class ClearValue(BaseValue): - """Class representing a clear/plaintext value (constant or not)""" + """Class representing a clear/plaintext value (constant or not).""" class EncryptedValue(BaseValue): - """Class representing an encrypted value (constant or not)""" + """Class representing an encrypted value (constant or not).""" diff --git a/hdk/common/debugging/__init__.py b/hdk/common/debugging/__init__.py index 16dbae9be..31dbfc844 100644 --- a/hdk/common/debugging/__init__.py +++ b/hdk/common/debugging/__init__.py @@ -1,2 +1,2 @@ -"""Module for debugging""" +"""Module for debugging.""" from .draw_graph import draw_graph, get_printable_graph diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index 8454dc5f4..63455f560 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -1,4 +1,4 @@ -"""functions to draw the different graphs we can generate in the package, eg to debug""" +"""functions to draw the different graphs we can generate in the package, eg to debug.""" from typing import Any, Dict, List import matplotlib.pyplot as plt @@ -19,7 +19,8 @@ IR_NODE_COLOR_MAPPING = { def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float = 1.0) -> Dict: - """ + """Returns positions for graphs, to make them easy to read. + Returns a pos to be used later with eg nx.draw_networkx_nodes, so that nodes are ordered by depth from input along the x axis and have a uniform distribution along the y axis @@ -33,7 +34,6 @@ def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float pos (Dict): the argument to use with eg nx.draw_networkx_nodes """ - nodes_depth = {node: 0 for node in graph.nodes()} input_nodes = [node for node in graph.nodes() if len(list(graph.predecessors(node))) == 0] @@ -89,8 +89,7 @@ def draw_graph( block_until_user_closes_graph: bool = True, draw_edge_numbers: bool = True, ) -> None: - """ - Draw a graph + """Draw a graph. Args: graph (OPGraph): The graph that we want to draw @@ -105,7 +104,6 @@ def draw_graph( None """ - assert isinstance(opgraph, OPGraph) set_of_nodes_which_are_outputs = set(opgraph.output_nodes.values()) graph = opgraph.graph @@ -212,7 +210,7 @@ def draw_graph( def data_type_to_string(node): - """Return the datatypes of the outputs of the node + """Return the datatypes of the outputs of the node. Args: node: a graph node @@ -225,7 +223,7 @@ def data_type_to_string(node): def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: - """Return a string representing a graph + """Return a string representing a graph. Args: graph (OPGraph): The graph that we want to draw @@ -235,7 +233,6 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: Returns: str: a string to print or save in a file """ - assert isinstance(opgraph, OPGraph) list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) graph = opgraph.graph diff --git a/hdk/common/extensions/__init__.py b/hdk/common/extensions/__init__.py index c86a0bf79..88bbd5e07 100644 --- a/hdk/common/extensions/__init__.py +++ b/hdk/common/extensions/__init__.py @@ -1,2 +1,2 @@ -"""Extensions module to provide additional functionality to our users""" +"""Extensions module to provide additional functionality to our users.""" from . import table diff --git a/hdk/common/extensions/table.py b/hdk/common/extensions/table.py index d4e362e85..38995f07b 100644 --- a/hdk/common/extensions/table.py +++ b/hdk/common/extensions/table.py @@ -1,4 +1,4 @@ -"""This file contains a wrapper class for direct table lookups""" +"""This file contains a wrapper class for direct table lookups.""" from copy import deepcopy from typing import Iterable, Tuple, Union @@ -11,7 +11,7 @@ from ..tracing.base_tracer import BaseTracer class LookupTable: - """Class representing a lookup table""" + """Class representing a lookup table.""" # lookup table itself, has 2^N entries table: Tuple[int, ...] diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index 04655eeba..09e5e4e23 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -1,4 +1,4 @@ -"""Code to wrap and make manipulating networkx graphs easier""" +"""Code to wrap and make manipulating networkx graphs easier.""" from copy import deepcopy from typing import Any, Dict, Iterable, Mapping @@ -13,7 +13,7 @@ from .tracing.tracing_helpers import create_graph_from_output_tracers class OPGraph: - """Class to make work with nx graphs easier""" + """Class to make work with nx graphs easier.""" graph: nx.MultiDiGraph input_nodes: Mapping[int, ir.Input] @@ -32,7 +32,7 @@ class OPGraph: } def evaluate(self, inputs: Mapping[int, Any]) -> Dict[ir.IntermediateNode, Any]: - """Function to evaluate a graph and get intermediate values for all nodes + """Function to evaluate a graph and get intermediate values for all nodes. Args: inputs (Mapping[int, Any]): The inputs to the program @@ -56,14 +56,15 @@ class OPGraph: return node_results def update_values_with_bounds(self, node_bounds: dict): - """Update nodes inputs and outputs values with data types able to hold data ranges measured - and passed in nodes_bounds + """Update values with bounds. + + Update nodes inputs and outputs values with data types able to hold data ranges measured + and passed in nodes_bounds Args: node_bounds (dict): Dictionary with nodes as keys, holding dicts with a 'min' and 'max' keys. Those bounds will be taken as the data range to be represented, per node. """ - node: ir.IntermediateNode for node in self.graph.nodes(): diff --git a/hdk/common/representation/__init__.py b/hdk/common/representation/__init__.py index 523d9963f..5a86259b7 100644 --- a/hdk/common/representation/__init__.py +++ b/hdk/common/representation/__init__.py @@ -1,2 +1,2 @@ -"""Representation module to represent source programs""" +"""Representation module to represent source programs.""" from . import intermediate diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index eef62fa8b..2ad4e1bc4 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -1,4 +1,4 @@ -"""File containing code to represent source programs operations""" +"""File containing code to represent source programs operations.""" from abc import ABC, abstractmethod from copy import deepcopy @@ -14,7 +14,7 @@ from ..data_types.values import ClearValue, EncryptedValue class IntermediateNode(ABC): - """Abstract Base Class to derive from to represent source program operations""" + """Abstract Base Class to derive from to represent source program operations.""" inputs: List[BaseValue] outputs: List[BaseValue] @@ -30,7 +30,7 @@ class IntermediateNode(ABC): self, inputs: Iterable[BaseValue], ) -> None: - + """__init__ for a binary operation, ie two inputs.""" IntermediateNode.__init__(self, inputs) assert len(self.inputs) == 2 @@ -38,6 +38,7 @@ class IntermediateNode(ABC): self.outputs = [mix_values_determine_holding_dtype(self.inputs[0], self.inputs[1])] def _is_equivalent_to_binary_commutative(self, other: object) -> bool: + """is_equivalent_to for a binary and commutative operation.""" return ( isinstance(other, self.__class__) and (self.inputs == other.inputs or self.inputs == other.inputs[::-1]) @@ -45,6 +46,7 @@ class IntermediateNode(ABC): ) def _is_equivalent_to_binary_non_commutative(self, other: object) -> bool: + """is_equivalent_to for a binary and non-commutative operation.""" return ( isinstance(other, self.__class__) and self.inputs == other.inputs @@ -53,8 +55,10 @@ class IntermediateNode(ABC): @abstractmethod def is_equivalent_to(self, other: object) -> bool: - """Overriding __eq__ has unwanted side effects, this provides the same facility without - disrupting expected behavior too much + """Alternative to __eq__ to check equivalence between IntermediateNodes. + + Overriding __eq__ has unwanted side effects, this provides the same facility without + disrupting expected behavior too much Args: other (object): Other object to check against @@ -70,7 +74,7 @@ class IntermediateNode(ABC): @abstractmethod def evaluate(self, inputs: Mapping[int, Any]) -> Any: - """Function to simulate what the represented computation would output for the given inputs + """Function to simulate what the represented computation would output for the given inputs. Args: inputs (Mapping[int, Any]): Mapping containing the inputs for the evaluation @@ -81,7 +85,7 @@ class IntermediateNode(ABC): class Add(IntermediateNode): - """Addition between two values""" + """Addition between two values.""" __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative @@ -91,7 +95,7 @@ class Add(IntermediateNode): class Sub(IntermediateNode): - """Subtraction between two values""" + """Subtraction between two values.""" __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_non_commutative @@ -101,7 +105,7 @@ class Sub(IntermediateNode): class Mul(IntermediateNode): - """Multiplication between two values""" + """Multiplication between two values.""" __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative @@ -111,7 +115,7 @@ class Mul(IntermediateNode): class Input(IntermediateNode): - """Node representing an input of the program""" + """Node representing an input of the program.""" input_name: str program_input_idx: int @@ -141,7 +145,7 @@ class Input(IntermediateNode): class ConstantInput(IntermediateNode): - """Node representing a constant of the program""" + """Node representing a constant of the program.""" constant_data: Scalars @@ -175,7 +179,7 @@ class ConstantInput(IntermediateNode): class ArbitraryFunction(IntermediateNode): - """Node representing a univariate arbitrary function, e.g. sin(x)""" + """Node representing a univariate arbitrary function, e.g. sin(x).""" # The arbitrary_func is not optional but mypy has a long standing bug and is not able to # understand this properly. See https://github.com/python/mypy/issues/708#issuecomment-605636623 diff --git a/hdk/common/tracing/__init__.py b/hdk/common/tracing/__init__.py index ac51d4934..e40ece012 100644 --- a/hdk/common/tracing/__init__.py +++ b/hdk/common/tracing/__init__.py @@ -1,4 +1,4 @@ -"""Module for basic tracing facilities""" +"""Module for basic tracing facilities.""" from .base_tracer import BaseTracer from .tracing_helpers import ( create_graph_from_output_tracers, diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index b3fa03fae..8549f3a3d 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -1,4 +1,4 @@ -"""This file holds the code that can be shared between tracers""" +"""This file holds the code that can be shared between tracers.""" from abc import ABC from typing import Iterable, List, Tuple, Type, Union @@ -9,7 +9,7 @@ from ..representation import intermediate as ir class BaseTracer(ABC): - """Base class for implementing tracers""" + """Base class for implementing tracers.""" inputs: List["BaseTracer"] traced_computation: ir.IntermediateNode @@ -30,7 +30,7 @@ class BaseTracer(ABC): inputs: Iterable[Union["BaseTracer", Scalars]], computation_to_trace: Type[ir.IntermediateNode], ) -> Tuple["BaseTracer", ...]: - """Helper functions to instantiate all output BaseTracer for a given computation + """Helper functions to instantiate all output BaseTracer for a given computation. Args: inputs (List[BaseTracer]): Previous BaseTracer used as inputs for a new node @@ -40,7 +40,6 @@ class BaseTracer(ABC): Returns: Tuple[BaseTracer, ...]: A tuple containing an BaseTracer per output function """ - # For inputs which are actually constant, first convert into a tracer def sanitize(inp): if not isinstance(inp, BaseTracer): @@ -61,7 +60,6 @@ class BaseTracer(ABC): return output_tracers def __add__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": - result_tracer = self.instantiate_output_tracers( [self, other], ir.Add, @@ -76,7 +74,6 @@ class BaseTracer(ABC): __radd__ = __add__ def __sub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": - result_tracer = self.instantiate_output_tracers( [self, other], ir.Sub, @@ -86,7 +83,6 @@ class BaseTracer(ABC): return result_tracer[0] def __rsub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": - result_tracer = self.instantiate_output_tracers( [other, self], ir.Sub, @@ -111,7 +107,7 @@ class BaseTracer(ABC): def make_const_input_tracer(tracer_class: Type[BaseTracer], constant_data: Scalars) -> BaseTracer: - """Helper function to create a tracer for a constant input + """Helper function to create a tracer for a constant input. Args: tracer_class (Type[BaseTracer]): the class of tracer to create a ConstantInput for diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py index 94bede3ec..087d042fb 100644 --- a/hdk/common/tracing/tracing_helpers.py +++ b/hdk/common/tracing/tracing_helpers.py @@ -1,4 +1,4 @@ -"""Helper functions for tracing""" +"""Helper functions for tracing.""" import collections from inspect import signature from typing import Callable, Dict, Iterable, OrderedDict, Set, Tuple, Type @@ -15,7 +15,7 @@ def make_input_tracers( tracer_class: Type[BaseTracer], function_parameters: OrderedDict[str, BaseValue], ) -> OrderedDict[str, BaseTracer]: - """Helper function to create tracers for a function's parameters + """Helper function to create tracers for a function's parameters. Args: tracer_class (Type[BaseTracer]): the class of tracer to create an Input for @@ -37,7 +37,7 @@ def make_input_tracer( input_idx: int, input_value: BaseValue, ) -> BaseTracer: - """Helper function to create a tracer for an input value + """Helper function to create a tracer for an input value. Args: tracer_class (Type[BaseTracer]): the class of tracer to create an Input for @@ -55,7 +55,7 @@ def make_input_tracer( def prepare_function_parameters( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] ) -> OrderedDict[str, BaseValue]: - """Function to filter the passed function_parameters to trace function_to_trace + """Function to filter the passed function_parameters to trace function_to_trace. Args: function_to_trace (Callable): function that will be traced for which parameters are checked @@ -87,7 +87,7 @@ def prepare_function_parameters( def create_graph_from_output_tracers( output_tracers: Iterable[BaseTracer], ) -> nx.MultiDiGraph: - """Generate a networkx Directed Graph that will represent the computation from a traced function + """Generate a networkx Directed Graph that represents the computation from a traced function. Args: output_tracers (Iterable[BaseTracer]): the output tracers resulting from running the diff --git a/hdk/hnumpy/__init__.py b/hdk/hnumpy/__init__.py index ddbd6bf7c..e310fe9b1 100644 --- a/hdk/hnumpy/__init__.py +++ b/hdk/hnumpy/__init__.py @@ -1,2 +1,2 @@ -"""Module for compiling numpy functions to homomorphic equivalents""" +"""Module for compiling numpy functions to homomorphic equivalents.""" from . import tracing diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 8662b46f9..f7954a65b 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -1,4 +1,4 @@ -"""hnumpy compilation function""" +"""hnumpy compilation function.""" from typing import Any, Callable, Dict, Iterator, Optional, Tuple @@ -17,7 +17,7 @@ def compile_numpy_function( dataset: Iterator[Tuple[Any, ...]], compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: - """Main API of hnumpy, to be able to compile an homomorphic program + """Main API of hnumpy, to be able to compile an homomorphic program. Args: function_to_trace (Callable): The function you want to trace @@ -33,7 +33,6 @@ def compile_numpy_function( OPGraph: currently returns a compilable graph, but later, it will return an MLIR compatible with the compiler, and even later, it will return the result of the compilation """ - # Trace op_graph = trace_numpy_function(function_to_trace, function_parameters) diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index a45333ce3..0830841d3 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -1,4 +1,4 @@ -"""File to hold code to manage package and numpy dtypes""" +"""File to hold code to manage package and numpy dtypes.""" from copy import deepcopy from typing import List @@ -26,7 +26,7 @@ SUPPORTED_TYPE_MSG_STRING = ", ".join(sorted(str(dtype) for dtype in SUPPORTED_N def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: - """Helper function to get the corresponding type from a numpy dtype + """Helper function to get the corresponding type from a numpy dtype. Args: numpy_dtype (DTypeLike): Any python object that can be translated to a numpy.dtype @@ -37,7 +37,6 @@ def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: Returns: BaseDataType: The corresponding data type corresponding to the input numpy_dtype """ - # Normalize numpy_dtype normalized_numpy_dtype = numpy.dtype(numpy_dtype) corresponding_hdk_dtype = NUMPY_TO_HDK_TYPE_MAPPING.get(normalized_numpy_dtype, None) @@ -54,7 +53,7 @@ def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: def convert_common_dtype_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dtype: - """Convert a BaseDataType to corresponding numpy.dtype + """Convert a BaseDataType to corresponding numpy.dtype. Args: common_dtype (BaseDataType): dtype to convert to numpy.dtype @@ -96,7 +95,7 @@ def get_ufunc_numpy_output_dtype( ufunc: numpy.ufunc, input_dtypes: List[BaseDataType], ) -> List[numpy.dtype]: - """Function to record the output dtype of a numpy.ufunc given some input types + """Function to record the output dtype of a numpy.ufunc given some input types. Args: ufunc (numpy.ufunc): The numpy.ufunc whose output types need to be recorded diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 912b40d88..245bbf641 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,4 +1,4 @@ -"""hnumpy tracing utilities""" +"""hnumpy tracing utilities.""" from typing import Callable, Dict, Mapping import numpy @@ -15,12 +15,12 @@ from .np_dtypes_helpers import ( class NPTracer(BaseTracer): - """Tracer class for numpy operations""" + """Tracer class for numpy operations.""" def __array_ufunc__(self, ufunc, method, *input_tracers, **kwargs): - """ - Catch calls to numpy ufunc and routes them to tracing functions if supported - read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch + """Catch calls to numpy ufunc and routes them to tracing functions if supported. + + Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch """ if method == "__call__": tracing_func = self.get_tracing_func_for_np_ufunc(ufunc) @@ -31,8 +31,10 @@ class NPTracer(BaseTracer): raise NotImplementedError("Only __call__ method is supported currently") def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": - """Support numpy astype feature, for now it only accepts a dtype and no additional - parameters, *args and **kwargs are accepted for interface compatibility only + r"""Support numpy astype feature. + + For now it only accepts a dtype and no additional parameters, \*args and + \*\*kwargs are accepted for interface compatibility only Args: numpy_dtype (DTypeLike): The object describing a numpy type @@ -59,7 +61,7 @@ class NPTracer(BaseTracer): @staticmethod def get_tracing_func_for_np_ufunc(ufunc: numpy.ufunc) -> Callable: - """Get the tracing function for a numpy ufunc + """Get the tracing function for a numpy ufunc. Args: ufunc (numpy.ufunc): The numpy ufunc that will be traced @@ -88,7 +90,7 @@ class NPTracer(BaseTracer): return common_output_dtypes def rint(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.rint + """Function to trace numpy.rint. Returns: NPTracer: The output NPTracer containing the traced function @@ -116,7 +118,7 @@ class NPTracer(BaseTracer): def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] ) -> OPGraph: - """Function used to trace a numpy function + """Function used to trace a numpy function. Args: function_to_trace (Callable): The function you want to trace diff --git a/poetry.lock b/poetry.lock index dbe18bb63..ef3c23ba3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -474,6 +474,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + [[package]] name = "pygments" version = "2.9.0" @@ -797,7 +811,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "5e466fc94c468da8f58c9b948206b3589f2253c54f0f3f14520a0f08334fe730" +content-hash = "e5b217fd28a7ed316b4bcc779ffb6616596b4b596293fc1e87cda7dceeccc7f8" [metadata.files] alabaster = [ @@ -1149,6 +1163,11 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pillow = [ + {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, + {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, + {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, + {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, + {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, @@ -1192,6 +1211,10 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] pygments = [ {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, diff --git a/pyproject.toml b/pyproject.toml index ffb81ebd7..747e9a5c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ pytest = "^6.2.4" pytest-cov = "^2.12.1" diff-cover = "^6.2.0" mypy = "^0.910" +pydocstyle = "^6.1.1" [build-system] requires = ["poetry-core>=1.0.0"] From 8fd0ae5c8510d1cbdaed3fdea8450a872b8d6eda Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 11 Aug 2021 16:43:25 +0300 Subject: [PATCH 0078/1104] feat(docker): create a docker environment to test and develop MLIR stuff --- .dockerignore | 1 + .gitignore | 1 + Makefile | 18 ++++++++++++++++++ docker/Dockerfile | 12 ++++++++++++ docs/dev/GETTING-STARTED.md | 22 ++++++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 .dockerignore create mode 100644 docker/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..1d085cacc --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +** diff --git a/.gitignore b/.gitignore index 7ba41e14c..d099f2934 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,7 @@ celerybeat.pid # Environments .env .venv +.docker_venv env/ venv/ ENV/ diff --git a/Makefile b/Makefile index 3e5e4fe46..6a7d43d9b 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,24 @@ coverage: @if [[ "$$BB" == "" ]]; then BB=origin/main; fi && poetry run diff-cover coverage.xml --fail-under 100 --html-report coverage.html --compare-branch $$BB .PHONY: coverage +docker_build: + docker build -t hdk:mlir -f docker/Dockerfile . +.PHONY: docker_build + +docker_rebuild: + docker build --no-cache -t hdk:mlir -f docker/Dockerfile . +.PHONY: docker_rebuild + +docker_start: + docker run --rm -it --volume /"$$(pwd)":/hdk hdk:mlir # the slash before pwd is for Windows +.PHONY: docker_start + +docker_build_and_start: docker_build docker_start +.PHONY: docker_build_and_start + +docker_bas: docker_build_and_start +.PHONY: docker_bas + docs: @# Generate the auto summary of documentations poetry run sphinx-apidoc -o docs/_apidoc hdk diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..1ac1f9494 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/zama-ai/zamalang-compiler + +RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-is-python3 && \ + pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir poetry && \ + echo "python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ + echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ + echo "cd /hdk/ && make setup_env" >> /root/.bashrc + +WORKDIR /hdk + +ENTRYPOINT ["/bin/bash", "-l"] diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 1c93bbf53..eecf7b00f 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -85,6 +85,28 @@ In this section, we will discuss the module structure of hdk briefly. You are en - tracing: utilities for generic function tracing used during intermediate representation creation - hnumpy: numpy frontend of hdk +## Working in Docker + +Before you start this section, go ahead and install docker. You can follow [this](https://docs.docker.com/engine/install/) official guide for that. + +Docker image of `hdk` is based of another docker image provided by the compiler team. The process of building on top of that image is automated, but it requires authorization. So to work in docker, talk with your team lead to gain access to the base docker image. You need to be added to a special group in the organization. + +Upon joining to the team, you need to log in using the following command: + +```shell +docker login ghcr.io +``` + +This command will ask for a username and a password. For username, just enter your GitHub username. For password, you should create a personal access token from [here](https://github.com/settings/tokens) selecting `read:packages` permission. Just paste the generated access token as your password, and you are good to go. + +Once you do that you can get inside the docker environment using the following command: + +```shell +make docker_build_and_start +``` + +After you finish your work, you can leave the docker by using the `exit` command or by pressing `CTRL + D`. + ## Contributing Now, you have a working environment, and you know what is where in the project. You are ready to contribute! Well, not so fast let's go over some other important things that you need to be careful about. From 4f6103d1d1439a6addf299dd909fede49d478002 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 12 Aug 2021 11:59:15 +0200 Subject: [PATCH 0079/1104] fix: fixing issue in the graph generation closes #130 --- hdk/common/tracing/tracing_helpers.py | 10 +++--- tests/hnumpy/test_debugging.py | 45 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py index 087d042fb..f60a33e72 100644 --- a/hdk/common/tracing/tracing_helpers.py +++ b/hdk/common/tracing/tracing_helpers.py @@ -1,7 +1,7 @@ """Helper functions for tracing.""" import collections from inspect import signature -from typing import Callable, Dict, Iterable, OrderedDict, Set, Tuple, Type +from typing import Callable, Dict, Iterable, OrderedDict, Set, Type import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph @@ -100,10 +100,12 @@ def create_graph_from_output_tracers( graph = nx.MultiDiGraph() visited_tracers: Set[BaseTracer] = set() - current_tracers = tuple(output_tracers) + # use dict as ordered set + current_tracers = {tracer: None for tracer in output_tracers} while current_tracers: - next_tracers: Tuple[BaseTracer, ...] = tuple() + # use dict as ordered set + next_tracers: Dict[BaseTracer, None] = dict() for tracer in current_tracers: current_ir_node = tracer.traced_computation graph.add_node(current_ir_node, content=current_ir_node) @@ -113,7 +115,7 @@ def create_graph_from_output_tracers( graph.add_node(input_ir_node, content=input_ir_node) graph.add_edge(input_ir_node, current_ir_node, input_idx=input_idx) if input_tracer not in visited_tracers: - next_tracers += (input_tracer,) + next_tracers.update({input_tracer: None}) visited_tracers.add(tracer) diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 750f56bbd..6a51a4374 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -8,6 +8,30 @@ from hdk.common.debugging import draw_graph, get_printable_graph from hdk.hnumpy import tracing +def issue_130_a(x, y): + """Test case derived from issue #130""" + # pylint: disable=unused-argument + intermediate = x + 1 + return (intermediate, intermediate) + # pylint: enable=unused-argument + + +def issue_130_b(x, y): + """Test case derived from issue #130""" + # pylint: disable=unused-argument + intermediate = x - 1 + return (intermediate, intermediate) + # pylint: enable=unused-argument + + +def issue_130_c(x, y): + """Test case derived from issue #130""" + # pylint: disable=unused-argument + intermediate = 1 - x + return (intermediate, intermediate) + # pylint: enable=unused-argument + + @pytest.mark.parametrize( "lambda_f,ref_graph_str", [ @@ -62,6 +86,27 @@ from hdk.hnumpy import tracing lambda x, y: (x, x + 1), "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%0, %2)", ), + ( + lambda x, y: (x + 1, x + 1), + "\n%0 = x" + "\n%1 = ConstantInput(1)" + "\n%2 = ConstantInput(1)" + "\n%3 = Add(0, 1)" + "\n%4 = Add(0, 2)" + "\nreturn(%3, %4)", + ), + ( + issue_130_a, + "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%2, %2)", + ), + ( + issue_130_b, + "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Sub(0, 1)\nreturn(%2, %2)", + ), + ( + issue_130_c, + "\n%0 = ConstantInput(1)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2, %2)", + ), ], ) @pytest.mark.parametrize( From 4afc373a6b80334b6663b7bc587b14ed27825ed3 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier <64148533+bcm-at-zama@users.noreply.github.com> Date: Thu, 12 Aug 2021 15:43:32 +0200 Subject: [PATCH 0080/1104] Update GETTING-STARTED.md Adding the group name --- docs/dev/GETTING-STARTED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index eecf7b00f..2c8577131 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -89,7 +89,7 @@ In this section, we will discuss the module structure of hdk briefly. You are en Before you start this section, go ahead and install docker. You can follow [this](https://docs.docker.com/engine/install/) official guide for that. -Docker image of `hdk` is based of another docker image provided by the compiler team. The process of building on top of that image is automated, but it requires authorization. So to work in docker, talk with your team lead to gain access to the base docker image. You need to be added to a special group in the organization. +Docker image of `hdk` is based of another docker image provided by the compiler team. The process of building on top of that image is automated, but it requires authorization. So to work in docker, talk with your team lead to gain access to the base docker image. You need to be added to a special group in the organization called `homomorphizer-ghcr`. Upon joining to the team, you need to log in using the following command: From 2c3c0809232cf682e7983201678d18a10996250d Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 12 Aug 2021 16:06:17 +0300 Subject: [PATCH 0081/1104] test(direct-tlu): create debugging and compilation tests for direct table lookup --- hdk/common/extensions/table.py | 24 +++++++--- tests/hnumpy/test_compile.py | 35 ++++++++++++++ tests/hnumpy/test_debugging.py | 84 +++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/hdk/common/extensions/table.py b/hdk/common/extensions/table.py index 38995f07b..0ef1e0041 100644 --- a/hdk/common/extensions/table.py +++ b/hdk/common/extensions/table.py @@ -30,19 +30,19 @@ class LookupTable: self.table = table self.output_dtype = make_integer_to_hold_ints(table, force_signed=False) - def __getitem__(self, item: Union[int, BaseTracer]): + def __getitem__(self, key: Union[int, BaseTracer]): # if a tracer is used for indexing, # we need to create an `ArbitraryFunction` node # because the result will be determined during the runtime - if isinstance(item, BaseTracer): + if isinstance(key, BaseTracer): traced_computation = ir.ArbitraryFunction( - input_base_value=item.output, - arbitrary_func=lambda x, table: table[x], + input_base_value=key.output, + arbitrary_func=LookupTable._checked_indexing, output_dtype=self.output_dtype, op_kwargs={"table": deepcopy(self.table)}, ) - return item.__class__( - inputs=[item], + return key.__class__( + inputs=[key], traced_computation=traced_computation, output_index=0, ) @@ -50,4 +50,14 @@ class LookupTable: # if not, it means table is indexed with a constant # thus, the result of the lookup is a constant # so, we can propagate it directly - return self.table[item] + return LookupTable._checked_indexing(key, self.table) + + @staticmethod + def _checked_indexing(x, table): + if x < 0 or x >= len(table): + raise ValueError( + f"Lookup table with {len(table)} entries cannot be indexed with {x} " + f"(you should check your dataset)", + ) + + return table[x] diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index fb7a76147..e6f4b8996 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -6,6 +6,7 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import EncryptedValue from hdk.common.debugging import draw_graph, get_printable_graph +from hdk.common.extensions.table import LookupTable from hdk.hnumpy.compile import compile_numpy_function @@ -45,3 +46,37 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) print(f"\n{str_of_the_graph}\n") + + +def test_compile_function_with_direct_tlu(): + """Test compile_numpy_function for a program with direct table lookup""" + + table = LookupTable([9, 2, 4, 11]) + + def function(x): + return x + table[x] + + op_graph = compile_numpy_function( + function, + {"x": EncryptedValue(Integer(2, is_signed=False))}, + iter([(0,), (1,), (2,), (3,)]), + ) + + str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) + print(f"\n{str_of_the_graph}\n") + + +def test_compile_function_with_direct_tlu_overflow(): + """Test compile_numpy_function for a program with direct table lookup overflow""" + + table = LookupTable([9, 2, 4, 11]) + + def function(x): + return table[x] + + with pytest.raises(ValueError): + compile_numpy_function( + function, + {"x": EncryptedValue(Integer(3, is_signed=False))}, + iter([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)]), + ) diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 6a51a4374..9e5972a1b 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -5,8 +5,12 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.debugging import draw_graph, get_printable_graph +from hdk.common.extensions.table import LookupTable from hdk.hnumpy import tracing +LOOKUP_TABLE_FROM_2B_TO_4B = LookupTable([9, 2, 4, 11]) +LOOKUP_TABLE_FROM_3B_TO_2B = LookupTable([0, 1, 3, 2, 2, 3, 1, 0]) + def issue_130_a(x, y): """Test case derived from issue #130""" @@ -143,6 +147,39 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): assert str_of_the_graph == ref_graph_str +@pytest.mark.parametrize( + "lambda_f,params,ref_graph_str", + [ + ( + lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], + {"x": EncryptedValue(Integer(2, is_signed=False))}, + "\n%0 = x\n%1 = ArbitraryFunction(0)\nreturn(%1)", + ), + ( + lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], + {"x": EncryptedValue(Integer(2, is_signed=False))}, + "\n%0 = x" + "\n%1 = ConstantInput(4)" + "\n%2 = Add(0, 1)" + "\n%3 = ArbitraryFunction(2)" + "\nreturn(%3)", + ), + ], +) +def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph_str): + "Test hnumpy get_printable_graph and draw_graph on graphs with direct table lookup" + graph = tracing.trace_numpy_function(lambda_f, params) + + draw_graph(graph, block_until_user_closes_graph=False) + + str_of_the_graph = get_printable_graph(graph) + + print(f"\nGot {str_of_the_graph}\n") + print(f"\nExp {ref_graph_str}\n") + + assert str_of_the_graph == ref_graph_str + + # Remark that the bitwidths are not particularly correct (eg, a MUL of a 17b times 23b # returning 23b), since they are replaced later by the real bitwidths computed on the # dataset @@ -174,7 +211,7 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): ], ) def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): - "Test hnumpy get_printable_graph with show_data_types" + """Test hnumpy get_printable_graph with show_data_types""" x, y = x_y graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) @@ -184,3 +221,48 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): print(f"\nExp {ref_graph_str}\n") assert str_of_the_graph == ref_graph_str + + +@pytest.mark.parametrize( + "lambda_f,params,ref_graph_str", + [ + ( + lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], + {"x": EncryptedValue(Integer(2, is_signed=False))}, + "\n%0 = x # Integer" + "\n%1 = ArbitraryFunction(0) # Integer" + "\nreturn(%1)", + ), + ( + lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], + {"x": EncryptedValue(Integer(2, is_signed=False))}, + "\n%0 = x # Integer" + "\n%1 = ConstantInput(4) # Integer" + "\n%2 = Add(0, 1) # Integer" + "\n%3 = ArbitraryFunction(2) # Integer" + "\nreturn(%3)", + ), + ( + lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[LOOKUP_TABLE_FROM_3B_TO_2B[x + 4]], + {"x": EncryptedValue(Integer(2, is_signed=False))}, + "\n%0 = x # Integer" + "\n%1 = ConstantInput(4) # Integer" + "\n%2 = Add(0, 1) # Integer" + "\n%3 = ArbitraryFunction(2) # Integer" + "\n%4 = ArbitraryFunction(3) # Integer" + "\nreturn(%4)", + ), + ], +) +def test_hnumpy_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref_graph_str): + """Test hnumpy get_printable_graph with show_data_types on graphs with direct table lookup""" + graph = tracing.trace_numpy_function(lambda_f, params) + + draw_graph(graph, block_until_user_closes_graph=False) + + str_of_the_graph = get_printable_graph(graph, show_data_types=True) + + print(f"\nGot {str_of_the_graph}\n") + print(f"\nExp {ref_graph_str}\n") + + assert str_of_the_graph == ref_graph_str From f6c9618b5a3b8fc9275d6bd662572c6ac62f1a66 Mon Sep 17 00:00:00 2001 From: Ayoub Benaissa Date: Fri, 13 Aug 2021 12:50:31 +0100 Subject: [PATCH 0082/1104] feat(mlir): MLIR Conversion (#103) * feat(mlir): conversion from HDKIR to MLIR * feat(mlir): support ir.Sub and ir.Mul - better type conversion from HDK to MLIR - Context management inside the converter class - better handling of input type in conversion functions * refactor(mlir): use input and output from OPGraph Co-authored-by: Arthur Meyre * feat(mlir): eint-int subtractions * feat(mlir): adhere to spec for supported ops * feat(OPGraph): getters for ordered inputs/outputs + formatting * tests(mlir): test converion via compiler roundtrip * fix(mlir): flip operands on int_eint sym ops * feat(mlir): check that the outputs are unsigned * feat(mlir): set bit_width of all nodes to the max This is currently required as the compiler is already assuming this. Could be removed from HDK when the compiler can do it on its own * feat: value_is_integer + CRs disable some linting errors * tests: update compile tests + coverage * refactor: reorganize mlir package + better doc * doc: conformance with pydocstyle Co-authored-by: Arthur Meyre --- hdk/common/data_types/__init__.py | 1 + hdk/common/data_types/dtypes_helpers.py | 28 +++- hdk/common/mlir/__init__.py | 5 + hdk/common/mlir/converters.py | 118 ++++++++++++++++ hdk/common/mlir/mlir_converter.py | 117 ++++++++++++++++ hdk/common/mlir/utils.py | 59 ++++++++ hdk/common/operator_graph.py | 23 ++- hdk/hnumpy/compile.py | 11 ++ tests/common/mlir/test_converters.py | 19 +++ tests/common/mlir/test_mlir_converter.py | 171 +++++++++++++++++++++++ tests/hnumpy/test_compile.py | 29 +++- 11 files changed, 576 insertions(+), 5 deletions(-) create mode 100644 hdk/common/mlir/__init__.py create mode 100644 hdk/common/mlir/converters.py create mode 100644 hdk/common/mlir/mlir_converter.py create mode 100644 hdk/common/mlir/utils.py create mode 100644 tests/common/mlir/test_converters.py create mode 100644 tests/common/mlir/test_mlir_converter.py diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index fb67c003b..a89d9a3fb 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,3 +1,4 @@ """Module for data types code and data structures.""" from . import dtypes_helpers, integers, values +from .integers import Integer from .values import BaseValue diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 310fcd6bd..3d289205e 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -34,7 +34,7 @@ def value_is_encrypted_unsigned_integer(value_to_check: BaseValue) -> bool: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is an encrypted value of type Integer + bool: True if the passed value_to_check is an encrypted value of type Integer and unsigned """ return ( value_is_encrypted_integer(value_to_check) @@ -42,6 +42,32 @@ def value_is_encrypted_unsigned_integer(value_to_check: BaseValue) -> bool: ) +def value_is_clear_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is a clear integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is a clear value of type Integer + """ + return isinstance(value_to_check, ClearValue) and isinstance( + value_to_check.data_type, INTEGER_TYPES + ) + + +def value_is_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is of type integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is a value of type Integer + """ + return isinstance(value_to_check.data_type, INTEGER_TYPES) + + def find_type_to_hold_both_lossy( dtype1: BaseDataType, dtype2: BaseDataType, diff --git a/hdk/common/mlir/__init__.py b/hdk/common/mlir/__init__.py new file mode 100644 index 000000000..bb04b2887 --- /dev/null +++ b/hdk/common/mlir/__init__.py @@ -0,0 +1,5 @@ +"""MLIR conversion submodule.""" +from .converters import V0_OPSET_CONVERSION_FUNCTIONS +from .mlir_converter import MLIRConverter + +__all__ = ["MLIRConverter", "V0_OPSET_CONVERSION_FUNCTIONS"] diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py new file mode 100644 index 000000000..f9a680ac0 --- /dev/null +++ b/hdk/common/mlir/converters.py @@ -0,0 +1,118 @@ +"""Converter functions from HDKIR to MLIR. + +Converter functions all have the same signature `converter(node, preds, ir_to_mlir_node, ctx)` +- `node`: IntermediateNode to be converted +- `preds`: List of predecessors of `node` ordered as operands +- `ir_to_mlir_node`: Dict mapping intermediate nodes to MLIR nodes or values +- `ctx`: MLIR context +""" +# pylint: disable=no-name-in-module,no-member +from zamalang.dialects import hlfhe + +from ..data_types.dtypes_helpers import ( + value_is_clear_integer, + value_is_encrypted_unsigned_integer, +) +from ..representation import intermediate as ir + + +def add(node, preds, ir_to_mlir_node, ctx): + """Converter function for the addition intermediate node.""" + assert len(node.inputs) == 2, "addition should have two inputs" + assert len(node.outputs) == 1, "addition should have a single output" + if value_is_encrypted_unsigned_integer(node.inputs[0]) and value_is_clear_integer( + node.inputs[1] + ): + return _add_eint_int(node, preds, ir_to_mlir_node, ctx) + if value_is_encrypted_unsigned_integer(node.inputs[1]) and value_is_clear_integer( + node.inputs[0] + ): + # flip lhs and rhs + return _add_eint_int(node, preds[::-1], ir_to_mlir_node, ctx) + if value_is_encrypted_unsigned_integer(node.inputs[0]) and value_is_encrypted_unsigned_integer( + node.inputs[1] + ): + return _add_eint_eint(node, preds, ir_to_mlir_node, ctx) + raise TypeError( + f"Don't support addition between {type(node.inputs[0])} and {type(node.inputs[1])}" + ) + + +def _add_eint_int(node, preds, ir_to_mlir_node, ctx): + """Converter function for the addition intermediate node with operands (eint, int).""" + lhs_node, rhs_node = preds + lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] + return hlfhe.AddEintIntOp( + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + lhs, + rhs, + ).result + + +def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): + """Converter function for the addition intermediate node with operands (eint, int).""" + lhs_node, rhs_node = preds + lhs, rhs = lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] + return hlfhe.AddEintOp( + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + lhs, + rhs, + ).result + + +def sub(node, preds, ir_to_mlir_node, ctx): + """Converter function for the subtraction intermediate node.""" + assert len(node.inputs) == 2, "subtraction should have two inputs" + assert len(node.outputs) == 1, "subtraction should have a single output" + if value_is_clear_integer(node.inputs[0]) and value_is_encrypted_unsigned_integer( + node.inputs[1] + ): + return _sub_int_eint(node, preds, ir_to_mlir_node, ctx) + raise TypeError( + f"Don't support subtraction between {type(node.inputs[0])} and {type(node.inputs[1])}" + ) + + +def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): + """Converter function for the subtraction intermediate node with operands (int, eint).""" + lhs_node, rhs_node = preds + lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] + return hlfhe.SubIntEintOp( + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + lhs, + rhs, + ).result + + +def mul(node, preds, ir_to_mlir_node, ctx): + """Converter function for the multiplication intermediate node.""" + assert len(node.inputs) == 2, "multiplication should have two inputs" + assert len(node.outputs) == 1, "multiplication should have a single output" + if value_is_encrypted_unsigned_integer(node.inputs[0]) and value_is_clear_integer( + node.inputs[1] + ): + return _mul_eint_int(node, preds, ir_to_mlir_node, ctx) + if value_is_encrypted_unsigned_integer(node.inputs[1]) and value_is_clear_integer( + node.inputs[0] + ): + # flip lhs and rhs + return _mul_eint_int(node, preds[::-1], ir_to_mlir_node, ctx) + raise TypeError( + f"Don't support multiplication between {type(node.inputs[0])} and {type(node.inputs[1])}" + ) + + +def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): + """Converter function for the multiplication intermediate node with operands (eint, int).""" + lhs_node, rhs_node = preds + lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] + return hlfhe.MulEintIntOp( + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + lhs, + rhs, + ).result + + +V0_OPSET_CONVERSION_FUNCTIONS = {ir.Add: add, ir.Sub: sub, ir.Mul: mul} + +# pylint: enable=no-name-in-module,no-member diff --git a/hdk/common/mlir/mlir_converter.py b/hdk/common/mlir/mlir_converter.py new file mode 100644 index 000000000..2e443b785 --- /dev/null +++ b/hdk/common/mlir/mlir_converter.py @@ -0,0 +1,117 @@ +"""File containing code to convert a DAG containing ir nodes to the compiler opset.""" +# pylint: disable=no-name-in-module,no-member +from typing import cast + +import networkx as nx +import zamalang +from mlir.dialects import builtin +from mlir.ir import Context, InsertionPoint, IntegerType, Location, Module +from mlir.ir import Type as MLIRType +from zamalang.dialects import hlfhe + +from .. import data_types +from ..data_types import Integer +from ..data_types.dtypes_helpers import ( + value_is_clear_integer, + value_is_encrypted_unsigned_integer, +) +from ..operator_graph import OPGraph +from ..representation import intermediate as ir + + +class MLIRConverter: + """Converter of the HDKIR to MLIR.""" + + def __init__(self, conversion_functions: dict) -> None: + """Instantiate a converter with a given set of converters. + + Args: + conversion_functions (dict): mapping HDKIR nodes to functions that generate MLIR. + every function should have 4 arguments: + - node (IntermediateNode): the node itself to be converted + - operands (IntermediateNode): predecessors of node ordered as operands + - ir_to_mlir_node (dict): mapping between IntermediateNode and their equivalent + MLIR values + - context (mlir.Context): the MLIR context being used for the conversion + """ + self.conversion_functions = conversion_functions + self._init_context() + + def _init_context(self): + self.context = Context() + zamalang.register_dialects(self.context) + + def hdk_value_to_mlir_type(self, value: data_types.BaseValue) -> MLIRType: + """Convert an HDK value to its corresponding MLIR Type. + + Args: + value: value to convert + + Returns: + corresponding MLIR type + """ + if value_is_encrypted_unsigned_integer(value): + return hlfhe.EncryptedIntegerType.get( + self.context, cast(Integer, value.data_type).bit_width + ) + if value_is_clear_integer(value): + dtype = cast(Integer, value.data_type) + if dtype.is_signed: + return IntegerType.get_signed(dtype.bit_width, context=self.context) + return IntegerType.get_unsigned(dtype.bit_width, context=self.context) + raise TypeError(f"can't convert value of type {type(value)} to MLIR type") + + def convert(self, op_graph: OPGraph) -> str: + """Convert the graph of IntermediateNode to an MLIR textual representation. + + Args: + graph: graph of IntermediateNode to be converted + + Returns: + textual MLIR representation + """ + with self.context, Location.unknown(): + module = Module.create() + # collect inputs + with InsertionPoint(module.body): + func_types = [ + self.hdk_value_to_mlir_type(input_node.inputs[0]) + for input_node in op_graph.get_ordered_inputs() + ] + + @builtin.FuncOp.from_py_func(*func_types) + def fhe_circuit(*arg): + ir_to_mlir_node = {} + for arg_num, node in op_graph.input_nodes.items(): + ir_to_mlir_node[node] = arg[arg_num] + for node in nx.topological_sort(op_graph.graph): + if isinstance(node, ir.Input): + continue + mlir_op = self.conversion_functions.get(type(node), None) + if mlir_op is None: # pragma: no cover + raise NotImplementedError( + f"we don't yet support conversion to MLIR of computations using" + f"{type(node)}" + ) + # get sorted preds: sorted by their input index + # replication of pred is possible (e.g lambda x: x + x) + idx_to_pred = {} + for pred in op_graph.graph.pred[node]: + edge_data = op_graph.graph.get_edge_data(pred, node) + for data in edge_data.values(): + idx_to_pred[data["input_idx"]] = pred + preds = [idx_to_pred[i] for i in range(len(idx_to_pred))] + # convert to mlir + result = mlir_op(node, preds, ir_to_mlir_node, self.context) + ir_to_mlir_node[node] = result + + results = ( + ir_to_mlir_node[output_node] + for output_node in op_graph.get_ordered_outputs() + ) + return results + + return module.__str__() + + +# pylint: enable=no-name-in-module,no-member diff --git a/hdk/common/mlir/utils.py b/hdk/common/mlir/utils.py new file mode 100644 index 000000000..ff0b5c195 --- /dev/null +++ b/hdk/common/mlir/utils.py @@ -0,0 +1,59 @@ +"""Utilities for MLIR conversion.""" +from typing import cast + +from ..data_types import Integer +from ..data_types.dtypes_helpers import ( + value_is_clear_integer, + value_is_encrypted_integer, + value_is_integer, +) +from ..operator_graph import OPGraph + + +def is_graph_values_compatible_with_mlir(op_graph: OPGraph) -> bool: + """Make sure the graph outputs are unsigned integers, which is what the compiler supports. + + Args: + op_graph: computation graph to check + + Returns: + bool: is the graph compatible with the expected MLIR representation + """ + return all( + all( + value_is_integer(out) and not cast(Integer, out.data_type).is_signed + for out in out_node.outputs + ) + for out_node in op_graph.output_nodes.values() + ) + + +def _set_all_bit_width(op_graph: OPGraph, p: int): + """Set all bit_width in the graph to `p` and `p+1` for clear and encrypted values respectively. + + Args: + op_graph: graph to set bit_width for + p: bit_width to set everywhere + """ + for node in op_graph.graph.nodes: + for value in node.outputs + node.inputs: + if value_is_clear_integer(value): + value.data_type.bit_width = p + 1 + elif value_is_encrypted_integer(value): + value.data_type.bit_width = p + + +def update_bit_width_for_mlir(op_graph: OPGraph): + """Prepare bit_width of all nodes to be the same, set to the maximum value in the graph. + + Args: + op_graph: graph to update bit_width for + """ + max_bit_width = 0 + for node in op_graph.graph.nodes: + for value_out in node.outputs: + if value_is_clear_integer(value_out): + max_bit_width = max(max_bit_width, value_out.data_type.bit_width - 1) + elif value_is_encrypted_integer(value_out): + max_bit_width = max(max_bit_width, value_out.data_type.bit_width) + _set_all_bit_width(op_graph, max_bit_width) diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index 09e5e4e23..14bd2b539 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -1,7 +1,7 @@ """Code to wrap and make manipulating networkx graphs easier.""" from copy import deepcopy -from typing import Any, Dict, Iterable, Mapping +from typing import Any, Dict, Iterable, List, Mapping import networkx as nx @@ -31,6 +31,22 @@ class OPGraph: if len(self.graph.pred[node]) == 0 and isinstance(node, ir.Input) } + def get_ordered_inputs(self) -> List[ir.Input]: + """Get the input nodes of the graph, ordered by their index. + + Returns: + List[ir.Input]: ordered input nodes + """ + return [self.input_nodes[idx] for idx in range(len(self.input_nodes))] + + def get_ordered_outputs(self) -> List[ir.IntermediateNode]: + """Get the output nodes of the graph, ordered by their index. + + Returns: + List[ir.IntermediateNode]: ordered input nodes + """ + return [self.output_nodes[idx] for idx in range(len(self.output_nodes))] + def evaluate(self, inputs: Mapping[int, Any]) -> Dict[ir.IntermediateNode, Any]: """Function to evaluate a graph and get intermediate values for all nodes. @@ -69,7 +85,10 @@ class OPGraph: for node in self.graph.nodes(): current_node_bounds = node_bounds[node] - min_bound, max_bound = current_node_bounds["min"], current_node_bounds["max"] + min_bound, max_bound = ( + current_node_bounds["min"], + current_node_bounds["max"], + ) if not isinstance(node, ir.Input): for output_value in node.outputs: diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index f7954a65b..90b417e0e 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -7,6 +7,10 @@ from hdk.hnumpy.tracing import trace_numpy_function from ..common.compilation import CompilationArtifacts from ..common.data_types import BaseValue +from ..common.mlir.utils import ( + is_graph_values_compatible_with_mlir, + update_bit_width_for_mlir, +) from ..common.operator_graph import OPGraph from ..hnumpy.tracing import trace_numpy_function @@ -42,6 +46,13 @@ def compile_numpy_function( # Update the graph accordingly: after that, we have the compilable graph op_graph.update_values_with_bounds(node_bounds) + # Make sure the graph can be lowered to MLIR + if not is_graph_values_compatible_with_mlir(op_graph): + raise TypeError("signed integers aren't supported for MLIR lowering") + + # Update bit_width for MLIR + update_bit_width_for_mlir(op_graph) + # Fill compilation artifacts if compilation_artifacts is not None: compilation_artifacts.operation_graph = op_graph diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py new file mode 100644 index 000000000..26d100925 --- /dev/null +++ b/tests/common/mlir/test_converters.py @@ -0,0 +1,19 @@ +"""Test converter functions""" +import pytest + +from hdk.common.mlir.converters import add, mul, sub + + +class MockNode: + """Mocking an intermediate node""" + + def __init__(self, inputs=5, outputs=5): + self.inputs = [None for i in range(inputs)] + self.outputs = [None for i in range(outputs)] + + +@pytest.mark.parametrize("converter", [add, sub, mul]) +def test_failing_converter(converter): + """Test failing converter""" + with pytest.raises(TypeError, match=r"Don't support .* between .* and .*"): + converter(MockNode(2, 1), None, None, None) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py new file mode 100644 index 000000000..851405072 --- /dev/null +++ b/tests/common/mlir/test_mlir_converter.py @@ -0,0 +1,171 @@ +"""Test file for conversion to MLIR""" +# pylint: disable=no-name-in-module,no-member +import itertools + +import pytest +from mlir.ir import IntegerType +from zamalang import compiler +from zamalang.dialects import hlfhe + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearValue, EncryptedValue +from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter +from hdk.hnumpy.compile import compile_numpy_function + + +def add(x, y): + """Test simple add""" + return x + y + + +def sub(x, y): + """Test simple sub""" + return x - y + + +def mul(x, y): + """Test simple mul""" + return x * y + + +def sub_add_mul(x, y, z): + """Test combination of ops""" + return z - y + x * z + + +def ret_multiple(x, y, z): + """Test return of multiple values""" + return x, y, z + + +def ret_multiple_different_order(x, y, z): + """Test return of multiple values in a different order from input""" + return y, z, x + + +def datagen(*args): + """Generate data from ranges""" + for prod in itertools.product(*args): + yield prod + + +@pytest.mark.parametrize( + "func, args_dict, args_ranges", + [ + ( + add, + { + "x": EncryptedValue(Integer(64, is_signed=False)), + "y": ClearValue(Integer(32, is_signed=False)), + }, + (range(0, 8), range(1, 4)), + ), + ( + add, + { + "x": ClearValue(Integer(32, is_signed=False)), + "y": EncryptedValue(Integer(64, is_signed=False)), + }, + (range(0, 8), range(1, 4)), + ), + ( + add, + { + "x": EncryptedValue(Integer(7, is_signed=False)), + "y": EncryptedValue(Integer(7, is_signed=False)), + }, + (range(7, 15), range(1, 5)), + ), + ( + sub, + { + "x": ClearValue(Integer(8, is_signed=False)), + "y": EncryptedValue(Integer(7, is_signed=False)), + }, + (range(5, 10), range(2, 6)), + ), + ( + mul, + { + "x": EncryptedValue(Integer(7, is_signed=False)), + "y": ClearValue(Integer(8, is_signed=False)), + }, + (range(1, 5), range(2, 8)), + ), + ( + mul, + { + "x": ClearValue(Integer(8, is_signed=False)), + "y": EncryptedValue(Integer(7, is_signed=False)), + }, + (range(1, 5), range(2, 8)), + ), + ( + sub_add_mul, + { + "x": EncryptedValue(Integer(7, is_signed=False)), + "y": EncryptedValue(Integer(7, is_signed=False)), + "z": ClearValue(Integer(7, is_signed=False)), + }, + (range(0, 8), range(1, 5), range(5, 12)), + ), + ( + ret_multiple, + { + "x": EncryptedValue(Integer(7, is_signed=False)), + "y": EncryptedValue(Integer(7, is_signed=False)), + "z": ClearValue(Integer(7, is_signed=False)), + }, + (range(1, 5), range(1, 5), range(1, 5)), + ), + ( + ret_multiple_different_order, + { + "x": EncryptedValue(Integer(7, is_signed=False)), + "y": EncryptedValue(Integer(7, is_signed=False)), + "z": ClearValue(Integer(7, is_signed=False)), + }, + (range(1, 5), range(1, 5), range(1, 5)), + ), + ], +) +def test_mlir_converter(func, args_dict, args_ranges): + """Test the conversion to MLIR by calling the parser from the compiler""" + dataset = datagen(*args_ranges) + result_graph = compile_numpy_function(func, args_dict, dataset) + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + mlir_result = converter.convert(result_graph) + # testing that this doesn't raise an error + compiler.round_trip(mlir_result) + + +def test_hdk_encrypted_integer_to_mlir_type(): + """Test conversion of EncryptedValue into MLIR""" + value = EncryptedValue(Integer(7, is_signed=False)) + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + eint = converter.hdk_value_to_mlir_type(value) + assert eint == hlfhe.EncryptedIntegerType.get(converter.context, 7) + + +@pytest.mark.parametrize("is_signed", [True, False]) +def test_hdk_clear_integer_to_mlir_type(is_signed): + """Test conversion of ClearValue into MLIR""" + value = ClearValue(Integer(5, is_signed=is_signed)) + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + int_mlir = converter.hdk_value_to_mlir_type(value) + with converter.context: + if is_signed: + assert int_mlir == IntegerType.get_signed(5) + else: + assert int_mlir == IntegerType.get_unsigned(5) + + +def test_failing_hdk_to_mlir_type(): + """Test failing conversion of an unsupported type into MLIR""" + value = "random" + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + with pytest.raises(TypeError, match=r"can't convert value of type .* to MLIR type"): + converter.hdk_value_to_mlir_type(value) + + +# pylint: enable=no-name-in-module,no-member diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index e6f4b8996..098967130 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -14,11 +14,11 @@ from hdk.hnumpy.compile import compile_numpy_function "function,input_ranges,list_of_arg_names", [ pytest.param(lambda x: x + 42, ((-2, 2),), ["x"]), - pytest.param(lambda x, y: x + y + 8, ((-10, 2), (-4, 6)), ["x", "y"]), + pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), pytest.param(lambda x, y: (x + 1, y + 10), ((-1, 1), (3, 4)), ["x", "y"]), pytest.param( lambda x, y, z: (x + y + 1 - z, x * y + 42, z, z + 99), - ((-1, 1), (3, 4), (10, 20)), + ((4, 8), (3, 4), (0, 4)), ["x", "y", "z"], ), ], @@ -80,3 +80,28 @@ def test_compile_function_with_direct_tlu_overflow(): {"x": EncryptedValue(Integer(3, is_signed=False))}, iter([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)]), ) + + +@pytest.mark.parametrize( + "function,input_ranges,list_of_arg_names", + [ + pytest.param(lambda x: x - 10, ((-2, 2),), ["x"]), + ], +) +def test_fail_compile(function, input_ranges, list_of_arg_names): + """Test function compile_numpy_function for a program with signed values""" + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedValue(Integer(64, True)) for arg_name in list_of_arg_names + } + + with pytest.raises(TypeError, match=r"signed integers aren't supported for MLIR lowering"): + compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) From 5961d1630e82f841e51a5bbd9ba1edf4f83eb29b Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 13 Aug 2021 14:38:32 +0300 Subject: [PATCH 0083/1104] refactor(compilation-artifacts): enable showing data types during export --- hdk/common/compilation/artifacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index 5332d9772..baed5e957 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -68,7 +68,7 @@ class CompilationArtifacts: if self.operation_graph is not None: with open(output_directory.joinpath("graph.txt"), "w") as f: - f.write(f"{get_printable_graph(self.operation_graph)[1:]}\n") + f.write(f"{get_printable_graph(self.operation_graph, show_data_types=True)[1:]}\n") if self.bounds is not None: with open(output_directory.joinpath("bounds.txt"), "w") as f: From 3245d3e6734147656875ca8efc306d9b102eb9e3 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier <64148533+bcm-at-zama@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:10:35 +0200 Subject: [PATCH 0084/1104] Feat/user friendly arbitrary function name 144 (#149) * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 * feat: let the dev give a useful name for ArbitraryFunction might be useful to debug or understand what happens closes #144 Co-authored-by: Benoit Chevallier-Mames --- hdk/common/debugging/draw_graph.py | 19 +++++++++++++++---- hdk/common/extensions/table.py | 1 + hdk/common/representation/intermediate.py | 4 ++++ tests/common/extensions/test_table.py | 2 ++ tests/hnumpy/test_debugging.py | 16 ++++++---------- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index 63455f560..abb8a13aa 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -13,7 +13,8 @@ IR_NODE_COLOR_MAPPING = { ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green", - ir.ArbitraryFunction: "orange", + "ArbitraryFunction": "orange", + "TLU": "grey", "output": "magenta", } @@ -115,6 +116,8 @@ def draw_graph( def get_color(node): if node in set_of_nodes_which_are_outputs: return IR_NODE_COLOR_MAPPING["output"] + if isinstance(node, ir.ArbitraryFunction): + return IR_NODE_COLOR_MAPPING[node.op_name] return IR_NODE_COLOR_MAPPING[type(node)] color_map = [get_color(node) for node in graph.nodes()] @@ -127,6 +130,8 @@ def draw_graph( return node.input_name if isinstance(node, ir.ConstantInput): return str(node.constant_data) + if isinstance(node, ir.ArbitraryFunction): + return node.op_name return node.__class__.__name__ label_dict = {node: get_proper_name(node) for node in graph.nodes()} @@ -209,7 +214,7 @@ def draw_graph( plt.show(block=block_until_user_closes_graph) -def data_type_to_string(node): +def output_data_type_to_string(node): """Return the datatypes of the outputs of the node. Args: @@ -249,7 +254,13 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: elif isinstance(node, ir.ConstantInput): what_to_print = f"ConstantInput({node.constant_data})" else: - what_to_print = node.__class__.__name__ + "(" + + base_name = node.__class__.__name__ + + if isinstance(node, ir.ArbitraryFunction): + base_name = node.op_name + + what_to_print = base_name + "(" # Find all the names of the current predecessors of the node list_of_arg_name = [] @@ -273,7 +284,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: # Manage datatypes if show_data_types: - new_line = f"{new_line: <40s} # {data_type_to_string(node)}" + new_line = f"{new_line: <40s} # {output_data_type_to_string(node)}" returned_str += f"\n{new_line}" diff --git a/hdk/common/extensions/table.py b/hdk/common/extensions/table.py index 0ef1e0041..74845799d 100644 --- a/hdk/common/extensions/table.py +++ b/hdk/common/extensions/table.py @@ -40,6 +40,7 @@ class LookupTable: arbitrary_func=LookupTable._checked_indexing, output_dtype=self.output_dtype, op_kwargs={"table": deepcopy(self.table)}, + op_name="TLU", ) return key.__class__( inputs=[key], diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 2ad4e1bc4..0c98d8eb6 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -186,12 +186,14 @@ class ArbitraryFunction(IntermediateNode): arbitrary_func: Optional[Callable] op_args: Tuple[Any, ...] op_kwargs: Dict[str, Any] + op_name: str def __init__( self, input_base_value: BaseValue, arbitrary_func: Callable, output_dtype: BaseDataType, + op_name: Optional[str] = None, op_args: Optional[Tuple[Any, ...]] = None, op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: @@ -202,6 +204,7 @@ class ArbitraryFunction(IntermediateNode): self.op_kwargs = deepcopy(op_kwargs) if op_kwargs is not None else {} # TLU/PBS has an encrypted output self.outputs = [EncryptedValue(output_dtype)] + self.op_name = op_name if op_name is not None else self.__class__.__name__ def evaluate(self, inputs: Mapping[int, Any]) -> Any: # This is the continuation of the mypy bug workaround @@ -215,5 +218,6 @@ class ArbitraryFunction(IntermediateNode): isinstance(other, ArbitraryFunction) and self.op_args == other.op_args and self.op_kwargs == other.op_kwargs + and self.op_name == other.op_name and super().is_equivalent_to(other) ) diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 530263871..656426dc9 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -57,6 +57,7 @@ def test_lookup_table_encrypted_lookup(test_helpers): arbitrary_func=lambda x, table: table[x], output_dtype=table.output_dtype, op_kwargs={"table": deepcopy(table.table)}, + op_name="TLU", ) ref_graph.add_node(output_arbitrary_function, content=output_arbitrary_function) @@ -93,6 +94,7 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): arbitrary_func=lambda x, table: table[x], output_dtype=table.output_dtype, op_kwargs={"table": deepcopy(table.table)}, + op_name="TLU", ) ref_graph.add_node(intermediate_arbitrary_function, content=intermediate_arbitrary_function) diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 9e5972a1b..080ad56e3 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -153,16 +153,12 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x\n%1 = ArbitraryFunction(0)\nreturn(%1)", + "\n%0 = x\n%1 = TLU(0)\nreturn(%1)", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x" - "\n%1 = ConstantInput(4)" - "\n%2 = Add(0, 1)" - "\n%3 = ArbitraryFunction(2)" - "\nreturn(%3)", + "\n%0 = x\n%1 = ConstantInput(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)", ), ], ) @@ -230,7 +226,7 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedValue(Integer(2, is_signed=False))}, "\n%0 = x # Integer" - "\n%1 = ArbitraryFunction(0) # Integer" + "\n%1 = TLU(0) # Integer" "\nreturn(%1)", ), ( @@ -239,7 +235,7 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): "\n%0 = x # Integer" "\n%1 = ConstantInput(4) # Integer" "\n%2 = Add(0, 1) # Integer" - "\n%3 = ArbitraryFunction(2) # Integer" + "\n%3 = TLU(2) # Integer" "\nreturn(%3)", ), ( @@ -248,8 +244,8 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): "\n%0 = x # Integer" "\n%1 = ConstantInput(4) # Integer" "\n%2 = Add(0, 1) # Integer" - "\n%3 = ArbitraryFunction(2) # Integer" - "\n%4 = ArbitraryFunction(3) # Integer" + "\n%3 = TLU(2) # Integer" + "\n%4 = TLU(3) # Integer" "\nreturn(%4)", ), ], From 048bb61e8e52e278af6f54998e3df8f975514516 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 13 Aug 2021 17:35:16 +0200 Subject: [PATCH 0085/1104] fix(tools): add git to the docker image to be able to make coverage - move comment in Makefile and make it silent --- Makefile | 3 ++- docker/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6a7d43d9b..a15ee7242 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,8 @@ docker_rebuild: .PHONY: docker_rebuild docker_start: - docker run --rm -it --volume /"$$(pwd)":/hdk hdk:mlir # the slash before pwd is for Windows + @# the slash before pwd is for Windows + docker run --rm -it --volume /"$$(pwd)":/hdk hdk:mlir .PHONY: docker_start docker_build_and_start: docker_build docker_start diff --git a/docker/Dockerfile b/docker/Dockerfile index 1ac1f9494..dd34963ac 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/zama-ai/zamalang-compiler -RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-is-python3 && \ +RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-is-python3 git && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry && \ echo "python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ From 479176e368a9090401b95816e249de00beafa768 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 16 Aug 2021 15:19:14 +0300 Subject: [PATCH 0086/1104] doc(examples): create quantized linear and logistic regression examples --- .github/workflows/continuous-integration.yaml | 6 + Makefile | 10 + examples/QuantizedLinearRegression.ipynb | 758 ++++++++++ examples/QuantizedLogisticRegression.ipynb | 880 ++++++++++++ examples/figures/QuantizationVisualized.svg | 3 + poetry.lock | 1251 ++++++++++++++++- pyproject.toml | 2 + script/nbmake_utils/notebook_sanitize.py | 21 + script/nbmake_utils/notebook_test_timeout.py | 23 + torch_requirements.txt | 7 + 10 files changed, 2883 insertions(+), 78 deletions(-) create mode 100644 examples/QuantizedLinearRegression.ipynb create mode 100644 examples/QuantizedLogisticRegression.ipynb create mode 100644 examples/figures/QuantizationVisualized.svg create mode 100644 script/nbmake_utils/notebook_sanitize.py create mode 100644 script/nbmake_utils/notebook_test_timeout.py create mode 100644 torch_requirements.txt diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 8976a685c..c498d1933 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -60,6 +60,12 @@ jobs: if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} run: | make pytest + - name: Notebooks + if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + run: | + make strip_nb + make notebook_timeout + poetry run pytest --nbmake examples/*.ipynb - name: Test coverage id: coverage if: ${{ steps.pytest.outcome != 'skipped' && !cancelled() }} diff --git a/Makefile b/Makefile index a15ee7242..ae6779345 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,8 @@ SHELL:=/bin/bash setup_env: poetry install poetry run python -m pip install -U pip wheel setuptools + poetry run python -m pip install -r torch_requirements.txt \ + -f https://download.pytorch.org/whl/torch_stable.html .PHONY: setup_env sync_env: @@ -100,3 +102,11 @@ pydocstyle: @# From http://www.pydocstyle.org/en/stable/error_codes.html poetry run pydocstyle hdk --convention google --add-ignore=D1,D202 .PHONY: pydocstyle + +strip_nb: + poetry run python ./script/nbmake_utils/notebook_sanitize.py examples +.PHONY: strip_nb + +notebook_timeout: + poetry run python ./script/nbmake_utils/notebook_test_timeout.py examples +.PHONY: notebook_timeout diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb new file mode 100644 index 000000000..1b9aae5b3 --- /dev/null +++ b/examples/QuantizedLinearRegression.ipynb @@ -0,0 +1,758 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0fe629d6", + "metadata": {}, + "source": [ + "# Quantized Linear Regression\n", + "\n", + "Currently, **hdk** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" + ] + }, + { + "cell_type": "markdown", + "id": "d0cfb561", + "metadata": {}, + "source": [ + "### Let's start by importing some libraries to develop our linear regression model" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3c1d929c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "69d25f7c", + "metadata": {}, + "source": [ + "### And some helpers for visualization" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a89c1a6c", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "markdown", + "id": "7729c1de", + "metadata": {}, + "source": [ + "### We need a dataset, a handcrafted one for simplicity" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b77a9e82", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", + "y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" + ] + }, + { + "cell_type": "markdown", + "id": "cc8673ff", + "metadata": {}, + "source": [ + "### Let's visualize our dataset to get a grasp of it" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "35a98d1a", + "metadata": {}, + "outputs": [], + "source": [ + "plt.ioff()\n", + "fig, ax = plt.subplots(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "56703410", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "e31b82e8", + "metadata": {}, + "source": [ + "### Now, we need a model so let's define it\n", + "\n", + "The main purpose of this tutorial is not to train a linear regression model but to use it homomorphically. So we will not discuss about how the model is trained." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cc5e72a2", + "metadata": {}, + "outputs": [], + "source": [ + "class Model:\n", + " w = None\n", + " b = None\n", + "\n", + " def fit(self, x, y):\n", + " a = np.ones((x.shape[0], x.shape[1] + 1), dtype=np.float32)\n", + " a[:, 1:] = x\n", + "\n", + " regularization_contribution = np.identity(x.shape[1] + 1, dtype=np.float32)\n", + " regularization_contribution[0][0] = 0\n", + "\n", + " parameters = np.linalg.pinv(a.T @ a + regularization_contribution) @ a.T @ y\n", + "\n", + " self.b = parameters[0]\n", + " self.w = parameters[1:].reshape(-1, 1)\n", + "\n", + " return self\n", + "\n", + " def evaluate(self, x):\n", + " return x @ self.w + self.b" + ] + }, + { + "cell_type": "markdown", + "id": "cefd8346", + "metadata": {}, + "source": [ + "### And create one" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b9879f4d", + "metadata": {}, + "outputs": [], + "source": [ + "model = Model().fit(x, y)" + ] + }, + { + "cell_type": "markdown", + "id": "01cfc83f", + "metadata": {}, + "source": [ + "### Time to make some predictions" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "78356d37", + "metadata": {}, + "outputs": [], + "source": [ + "inputs = np.linspace(40, 210, 100).reshape(-1, 1)\n", + "predictions = model.evaluate(inputs)" + ] + }, + { + "cell_type": "markdown", + "id": "58160140", + "metadata": {}, + "source": [ + "### Let's visualize our predictions to see how our model performs" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2a623999", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax.plot(inputs, predictions, color=\"blue\")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "d3f39faa", + "metadata": {}, + "source": [ + "### As a bonus let's inspect the model parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7fa65211", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[2.669915]]\n", + "-3.2335143\n" + ] + } + ], + "source": [ + "print(model.w)\n", + "print(model.b)" + ] + }, + { + "cell_type": "markdown", + "id": "544d6e34", + "metadata": {}, + "source": [ + "They are floating point numbers and we can't directly work with them!" + ] + }, + { + "cell_type": "markdown", + "id": "abf310f2", + "metadata": {}, + "source": [ + "### So, let's abstract quantization\n", + "\n", + "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "a7b3b993", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import SVG\n", + "SVG(filename=\"figures/QuantizationVisualized.svg\")" + ] + }, + { + "cell_type": "markdown", + "id": "9cbd7e1d", + "metadata": {}, + "source": [ + "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a8bab855", + "metadata": {}, + "outputs": [], + "source": [ + "class QuantizationParameters:\n", + " def __init__(self, q, zp, n):\n", + " # q = scale factor = 1 / distance between consecutive values\n", + " # zp = zero point which is used to determine the beginning of the quantized range\n", + " # (quantized 0 = the beginning of the quantized range = zp * distance between consecutive values)\n", + " # n = number of bits\n", + " \n", + " # e.g.,\n", + " \n", + " # n = 2\n", + " # zp = 2\n", + " # q = 0.66\n", + " # distance between consecutive values = 1 / q = 1.5151\n", + " \n", + " # quantized 0 = zp / q = zp * distance between consecutive values = 3.0303\n", + " # quantized 1 = quantized 0 + distance between consecutive values = 4.5454\n", + " # quantized 2 = quantized 1 + distance between consecutive values = 6.0606\n", + " # quantized 3 = quantized 2 + distance between consecutive values = 7.5757\n", + " \n", + " self.q = q\n", + " self.zp = zp\n", + " self.n = n\n", + "\n", + "class QuantizedArray:\n", + " def __init__(self, values, parameters):\n", + " # values = quantized values\n", + " # parameters = parameters used during quantization\n", + " \n", + " # e.g.,\n", + " \n", + " # values = [1, 0, 2, 1]\n", + " # parameters = QuantizationParameters(q=0.66, zp=2, n=2)\n", + " \n", + " # original array = [4.5454, 3.0303, 6.0606, 4.5454]\n", + " \n", + " self.values = np.array(values)\n", + " self.parameters = parameters\n", + "\n", + " @staticmethod\n", + " def of(x, n):\n", + " if not isinstance(x, np.ndarray):\n", + " x = np.array(x)\n", + "\n", + " min_x = x.min()\n", + " max_x = x.max()\n", + "\n", + " if min_x == max_x: # encoding single valued arrays\n", + " \n", + " if min_x == 0.0: # encoding 0s\n", + " \n", + " # dequantization = (x_q + zp_x) / q_x = 0 --> q_x = 1 && zp_x = 0 && x_q = 0\n", + " q_x = 1\n", + " zp_x = 0\n", + " x_q = np.zeros(x.shape, dtype=np.uint)\n", + " \n", + " elif min_x < 0.0: # encoding negative scalars\n", + " \n", + " # dequantization = (x_q + zp_x) / q_x = -x --> q_x = 1 / x & zp_x = -1 & x_q = 0\n", + " q_x = abs(1 / min_x)\n", + " zp_x = -1\n", + " x_q = np.zeros(x.shape, dtype=np.uint)\n", + " \n", + " else: # encoding positive scalars\n", + " \n", + " # dequantization = (x_q + zp_x) / q_x = x --> q_x = 1 / x & zp_x = 0 & x_q = 1\n", + " q_x = 1 / min_x\n", + " zp_x = 0\n", + " x_q = np.ones(x.shape, dtype=np.uint)\n", + " \n", + " else: # encoding multi valued arrays\n", + " \n", + " # distance between consecutive values = range of x / number of different quantized values = (max_x - min_x) / (2^n - 1)\n", + " # q = 1 / distance between consecutive values\n", + " q_x = (2**n - 1) / (max_x - min_x)\n", + " \n", + " # zp = what should be added to 0 to get min_x -> min_x = (0 + zp) / q -> zp = min_x * q\n", + " zp_x = int(round(min_x * q_x))\n", + " \n", + " # x = (x_q + zp) / q -> x_q = (x * q) - zp\n", + " x_q = ((q_x * x) - zp_x).round().astype(np.uint)\n", + "\n", + " return QuantizedArray(x_q, QuantizationParameters(q_x, zp_x, n))\n", + "\n", + " def dequantize(self):\n", + " # x = (x_q + zp) / q\n", + " # x = (x_q + zp) / q\n", + " return (self.values.astype(np.float32) + float(self.parameters.zp)) / self.parameters.q\n", + "\n", + " def affine(self, w, b, min_y, max_y, n_y):\n", + " # the formulas used in this method was derived from the following equations\n", + " #\n", + " # x = (x_q + zp_x) / q_x\n", + " # w = (w_q + zp_w) / q_w\n", + " # b = (b_q + zp_b) / q_b\n", + " #\n", + " # (x * w) + b = ((x_q + zp_x) / q_x) * ((w_q + zp_w) / q_w) + ((b_q + zp_b) / q_b)\n", + " # = y = (y_q + zp_y) / q_y\n", + " #\n", + " # So, ((x_q + zp_x) / q_x) * ((w_q + zp_w) / q_w) + ((b_q + zp_b) / q_b) = (y_q + zp_y) / q_y\n", + " # We can calculate zp_y and q_y from min_y, max_y, n_y. So, the only unknown is y_q and it can be solved.\n", + "\n", + " x_q = self.values\n", + " w_q = w.values\n", + " b_q = b.values\n", + "\n", + " q_x = self.parameters.q\n", + " q_w = w.parameters.q\n", + " q_b = b.parameters.q\n", + "\n", + " zp_x = self.parameters.zp\n", + " zp_w = w.parameters.zp\n", + " zp_b = b.parameters.zp\n", + "\n", + " q_y = (2**n_y - 1) / (max_y - min_y)\n", + " zp_y = int(round(min_y * q_y))\n", + "\n", + " y_q = (q_y / (q_x * q_w)) * ((x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b))\n", + " y_q -= min_y * q_y\n", + " y_q = y_q.round().clip(0, 2**n_y - 1).astype(np.uint)\n", + "\n", + " return QuantizedArray(y_q, QuantizationParameters(q_y, zp_y, n_y))\n", + "\n", + "class QuantizedFunction:\n", + " def __init__(self, table):\n", + " self.table = table\n", + "\n", + " @staticmethod\n", + " def of(f, input_bits, output_bits):\n", + " domain = np.array(range(2**input_bits), dtype=np.uint)\n", + " table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", + " return QuantizedFunction(table)" + ] + }, + { + "cell_type": "markdown", + "id": "e5be0800", + "metadata": {}, + "source": [ + "### Let's quantize our model parameters\n", + "\n", + "Since the parameters only consist of scalars, we can use a single bit quantization." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3ec0ad9b", + "metadata": {}, + "outputs": [], + "source": [ + "parameter_bits = 1\n", + "\n", + "w_q = QuantizedArray.of(model.w, parameter_bits)\n", + "b_q = QuantizedArray.of(model.b, parameter_bits)" + ] + }, + { + "cell_type": "markdown", + "id": "b43c0371", + "metadata": {}, + "source": [ + "### And quantize our inputs" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "20cea447", + "metadata": {}, + "outputs": [], + "source": [ + "input_bits = 6\n", + "\n", + "x_q = QuantizedArray.of(inputs, input_bits)" + ] + }, + { + "cell_type": "markdown", + "id": "ca76b68d", + "metadata": {}, + "source": [ + "### Time to make quantized inference" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "8728e939", + "metadata": {}, + "outputs": [], + "source": [ + "output_bits = 7\n", + "\n", + "min_y = predictions.min()\n", + "max_y = predictions.max()\n", + "y_q = x_q.affine(w_q, b_q, min_y, max_y, output_bits)\n", + "\n", + "quantized_predictions = y_q.dequantize()" + ] + }, + { + "cell_type": "markdown", + "id": "ab782b4a", + "metadata": {}, + "source": [ + "### And visualize the results" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "9d2bb5da", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax.plot(inputs, quantized_predictions, color=\"black\")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "4834cdfc", + "metadata": {}, + "source": [ + "### Now it's time to make the inference homomorphic" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "fcf4ea26", + "metadata": {}, + "outputs": [], + "source": [ + "q_y = (2**output_bits - 1) / (max_y - min_y)\n", + "zp_y = int(round(min_y * q_y))\n", + "\n", + "q_x = x_q.parameters.q\n", + "q_w = w_q.parameters.q\n", + "q_b = b_q.parameters.q\n", + "\n", + "zp_x = x_q.parameters.zp\n", + "zp_w = w_q.parameters.zp\n", + "zp_b = b_q.parameters.zp\n", + "\n", + "x_q = x_q.values\n", + "w_q = w_q.values\n", + "b_q = b_q.values" + ] + }, + { + "cell_type": "markdown", + "id": "43e47369", + "metadata": {}, + "source": [ + "### Simplification to rescue!\n", + "\n", + "The `y_q` formula in `QuantizedArray.affine(...)` can be rewritten to make it easier to implement in homomorphically. Here is the breakdown.\n", + "```\n", + "(q_y / (q_x * q_w)) * ((x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b)) - (min_y * q_y)\n", + "^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^\n", + "constant (c1) can be done constant (c2) constant (c3) constant (c4)\n", + " on the circuit \n", + " \n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " can be done on the circuit\n", + " \n", + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "2de0cf20", + "metadata": {}, + "outputs": [], + "source": [ + "c1 = q_y / (q_x * q_w)\n", + "c2 = w_q + zp_w\n", + "c3 = (q_x * q_w / q_b) * (b_q + zp_b)\n", + "c4 = min_y * q_y\n", + "\n", + "f = lambda intermediate: (c1 * (intermediate + c3)) - c4\n", + "f_q = QuantizedFunction.of(f, input_bits + parameter_bits, output_bits)\n", + "\n", + "from hdk.common.extensions.table import LookupTable\n", + "table = LookupTable([int(entry) for entry in f_q.table])\n", + "\n", + "w_0 = int(c2.flatten()[0])\n", + "\n", + "def infer(x_0):\n", + " return table[(x_0 + zp_x) * w_0]" + ] + }, + { + "cell_type": "markdown", + "id": "93eb9499", + "metadata": {}, + "source": [ + "### Time to compile our quantized inference function" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "a80895fd", + "metadata": {}, + "outputs": [], + "source": [ + "from hdk.common.data_types.integers import Integer\n", + "from hdk.common.data_types.values import EncryptedValue\n", + "from hdk.hnumpy.compile import compile_numpy_function\n", + "\n", + "dataset = []\n", + "for x_i in x_q:\n", + " dataset.append((int(x_i[0]),))\n", + "\n", + "homomorphic_model = compile_numpy_function(\n", + " infer,\n", + " {\"x_0\": EncryptedValue(Integer(input_bits, is_signed=False))},\n", + " iter(dataset),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f0b08a0f", + "metadata": {}, + "source": [ + "### Here is the textual representation of the operation graph" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2cc4e11d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "%0 = ConstantInput(1) # Integer\n", + "%1 = x_0 # Integer\n", + "%2 = ConstantInput(15) # Integer\n", + "%3 = Add(1, 2) # Integer\n", + "%4 = Mul(3, 0) # Integer\n", + "%5 = ArbitraryFunction(4) # Integer\n", + "return(%5)\n" + ] + } + ], + "source": [ + "from hdk.common.debugging import get_printable_graph\n", + "print(get_printable_graph(homomorphic_model, show_data_types=True))" + ] + }, + { + "cell_type": "markdown", + "id": "ade14f17", + "metadata": {}, + "source": [ + "### Finally, it's time to make homomorphic inference\n", + "\n", + "Or, at least, simulate it until the compiler integration is complete." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "dd2d03d7", + "metadata": {}, + "outputs": [], + "source": [ + "homomorphic_predictions = []\n", + "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", + " evaluation = homomorphic_model.evaluate({0: x_i})\n", + " inference = QuantizedArray(evaluation[homomorphic_model.output_nodes[0]], y_q.parameters)\n", + " homomorphic_predictions.append(inference.dequantize())\n", + "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" + ] + }, + { + "cell_type": "markdown", + "id": "443fbc03", + "metadata": {}, + "source": [ + "### And visualize it" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "57050b5d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "53ecca94", + "metadata": {}, + "source": [ + "### Enjoy!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb new file mode 100644 index 000000000..37bac1437 --- /dev/null +++ b/examples/QuantizedLogisticRegression.ipynb @@ -0,0 +1,880 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0fe629d6", + "metadata": {}, + "source": [ + "# Quantized Logistic Regression\n", + "\n", + "Currently, **hdk** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" + ] + }, + { + "cell_type": "markdown", + "id": "d0cfb561", + "metadata": {}, + "source": [ + "### Let's start by importing some libraries to develop our logistic regression model" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3c1d929c", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import torch" + ] + }, + { + "cell_type": "markdown", + "id": "69d25f7c", + "metadata": {}, + "source": [ + "### And some helpers for visualization" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a89c1a6c", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "markdown", + "id": "7729c1de", + "metadata": {}, + "source": [ + "### We need a dataset, a handcrafted one for simplicity" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b77a9e82", + "metadata": {}, + "outputs": [], + "source": [ + "x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n", + "y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float()" + ] + }, + { + "cell_type": "markdown", + "id": "cc8673ff", + "metadata": {}, + "source": [ + "### Let's visualize our dataset to get a grasp of it" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "35a98d1a", + "metadata": {}, + "outputs": [], + "source": [ + "plt.ioff()\n", + "fig, ax = plt.subplots(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "56703410", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x_min, x_max = x[:, 0].min(), x[:, 0].max()\n", + "x_deviation = x_max - x_min\n", + "\n", + "y_min, y_max = x[:, 1].min(), x[:, 1].max()\n", + "y_deviation = y_max - y_min\n", + "\n", + "ax.set_xlim(x_min - (x_deviation / 10), x_max + (x_deviation / 10))\n", + "ax.set_ylim(y_min - (y_deviation / 10), y_max + (y_deviation / 10))\n", + "\n", + "ax.scatter(\n", + " np.array([x_i[0] for x_i, y_i in zip(x, y) if y_i == 0], dtype=np.float32),\n", + " np.array([x_i[1] for x_i, y_i in zip(x, y) if y_i == 0], dtype=np.float32),\n", + " marker=\"x\",\n", + " color=\"red\",\n", + ")\n", + "ax.scatter(\n", + " np.array([x_i[0] for x_i, y_i in zip(x, y) if y_i == 1], dtype=np.float32),\n", + " np.array([x_i[1] for x_i, y_i in zip(x, y) if y_i == 1], dtype=np.float32),\n", + " marker=\"o\",\n", + " color=\"blue\",\n", + ")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "e31b82e8", + "metadata": {}, + "source": [ + "### Now, we need a model so let's define it" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "cc5e72a2", + "metadata": {}, + "outputs": [], + "source": [ + "class Model(torch.nn.Module):\n", + " def __init__(self, n):\n", + " super(Model, self).__init__()\n", + " self.fc = torch.nn.Linear(n, 1)\n", + "\n", + " def forward(self, x):\n", + " output = torch.sigmoid(self.fc(x))\n", + " return output" + ] + }, + { + "cell_type": "markdown", + "id": "cefd8346", + "metadata": {}, + "source": [ + "### And create one\n", + "\n", + "The main purpose of this tutorial is not to train a logistic regression model but to use it homomorphically. So we will not discuss about how the model is trained." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b9879f4d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch: 1 | Loss: 0.530019998550415\n", + "Epoch: 101 | Loss: 0.1248268187046051\n", + "Epoch: 201 | Loss: 0.07593712955713272\n", + "Epoch: 301 | Loss: 0.05418260768055916\n", + "Epoch: 401 | Loss: 0.04199932515621185\n", + "Epoch: 501 | Loss: 0.03424343094229698\n", + "Epoch: 601 | Loss: 0.028883913531899452\n", + "Epoch: 701 | Loss: 0.024963364005088806\n", + "Epoch: 801 | Loss: 0.021973103284835815\n", + "Epoch: 901 | Loss: 0.019618362188339233\n", + "Epoch: 1001 | Loss: 0.017716625705361366\n", + "Epoch: 1101 | Loss: 0.01614907570183277\n", + "Epoch: 1201 | Loss: 0.014835075475275517\n", + "Epoch: 1301 | Loss: 0.013717765919864178\n", + "Epoch: 1401 | Loss: 0.01275621633976698\n", + "Epoch: 1501 | Loss: 0.011920095421373844\n" + ] + } + ], + "source": [ + "model = Model(x.shape[1])\n", + "\n", + "optimizer = torch.optim.SGD(model.parameters(), lr=1)\n", + "criterion = torch.nn.BCELoss()\n", + "\n", + "epochs = 1501\n", + "for e in range(1, epochs + 1):\n", + " optimizer.zero_grad()\n", + "\n", + " out = model(x)\n", + " loss = criterion(out, y)\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " if e % 100 == 1 or e == epochs:\n", + " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" + ] + }, + { + "cell_type": "markdown", + "id": "01cfc83f", + "metadata": {}, + "source": [ + "### Time to make some predictions" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "78356d37", + "metadata": {}, + "outputs": [], + "source": [ + "contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 250)\n", + "contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 250)\n", + "contour_plot_x_data, contour_plot_y_data = np.meshgrid(contour_plot_x_data, contour_plot_y_data)\n", + "\n", + "inputs = np.stack((contour_plot_x_data.flatten(), contour_plot_y_data.flatten()), axis=1)\n", + "predictions = model(torch.tensor(inputs).float()).detach().numpy()" + ] + }, + { + "cell_type": "markdown", + "id": "58160140", + "metadata": {}, + "source": [ + "### Let's visualize our predictions to see how our model performs" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2a623999", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "contour = ax.contourf(\n", + " contour_plot_x_data,\n", + " contour_plot_y_data,\n", + " predictions.round().reshape(contour_plot_x_data.shape),\n", + " cmap=\"gray\",\n", + " alpha=0.50,\n", + ")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "d3f39faa", + "metadata": {}, + "source": [ + "### As a bonus let's inspect the model parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7fa65211", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[4.54424667]\n", + " [2.37960148]]\n", + "-14.69552993774414\n" + ] + } + ], + "source": [ + "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", + "b = model.fc.bias.flatten().tolist()[0]\n", + "\n", + "print(w)\n", + "print(b)" + ] + }, + { + "cell_type": "markdown", + "id": "544d6e34", + "metadata": {}, + "source": [ + "They are floating point numbers and we can't directly work with them!" + ] + }, + { + "cell_type": "markdown", + "id": "abf310f2", + "metadata": {}, + "source": [ + "### So, let's abstract quantization\n", + "\n", + "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d3ab2aa2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import SVG\n", + "SVG(filename=\"figures/QuantizationVisualized.svg\")" + ] + }, + { + "cell_type": "markdown", + "id": "e4314038", + "metadata": {}, + "source": [ + "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a8bab855", + "metadata": {}, + "outputs": [], + "source": [ + "class QuantizationParameters:\n", + " def __init__(self, q, zp, n):\n", + " # q = scale factor = 1 / distance between consecutive values\n", + " # zp = zero point which is used to determine the beginning of the quantized range\n", + " # (quantized 0 = the beginning of the quantized range = zp * distance between consecutive values)\n", + " # n = number of bits\n", + " \n", + " # e.g.,\n", + " \n", + " # n = 2\n", + " # zp = 2\n", + " # q = 0.66\n", + " # distance between consecutive values = 1 / q = 1.5151\n", + " \n", + " # quantized 0 = zp / q = zp * distance between consecutive values = 3.0303\n", + " # quantized 1 = quantized 0 + distance between consecutive values = 4.5454\n", + " # quantized 2 = quantized 1 + distance between consecutive values = 6.0606\n", + " # quantized 3 = quantized 2 + distance between consecutive values = 7.5757\n", + " \n", + " self.q = q\n", + " self.zp = zp\n", + " self.n = n\n", + "\n", + "class QuantizedArray:\n", + " def __init__(self, values, parameters):\n", + " # values = quantized values\n", + " # parameters = parameters used during quantization\n", + " \n", + " # e.g.,\n", + " \n", + " # values = [1, 0, 2, 1]\n", + " # parameters = QuantizationParameters(q=0.66, zp=2, n=2)\n", + " \n", + " # original array = [4.5454, 3.0303, 6.0606, 4.5454]\n", + " \n", + " self.values = np.array(values)\n", + " self.parameters = parameters\n", + "\n", + " @staticmethod\n", + " def of(x, n):\n", + " if not isinstance(x, np.ndarray):\n", + " x = np.array(x)\n", + "\n", + " min_x = x.min()\n", + " max_x = x.max()\n", + "\n", + " if min_x == max_x: # encoding single valued arrays\n", + " \n", + " if min_x == 0.0: # encoding 0s\n", + " \n", + " # dequantization = (x_q + zp_x) / q_x = 0 --> q_x = 1 && zp_x = 0 && x_q = 0\n", + " q_x = 1\n", + " zp_x = 0\n", + " x_q = np.zeros(x.shape, dtype=np.uint)\n", + " \n", + " elif min_x < 0.0: # encoding negative scalars\n", + " \n", + " # dequantization = (x_q + zp_x) / q_x = -x --> q_x = 1 / x & zp_x = -1 & x_q = 0\n", + " q_x = abs(1 / min_x)\n", + " zp_x = -1\n", + " x_q = np.zeros(x.shape, dtype=np.uint)\n", + " \n", + " else: # encoding positive scalars\n", + " \n", + " # dequantization = (x_q + zp_x) / q_x = x --> q_x = 1 / x & zp_x = 0 & x_q = 1\n", + " q_x = 1 / min_x\n", + " zp_x = 0\n", + " x_q = np.ones(x.shape, dtype=np.uint)\n", + " \n", + " else: # encoding multi valued arrays\n", + " \n", + " # distance between consecutive values = range of x / number of different quantized values = (max_x - min_x) / (2^n - 1)\n", + " # q = 1 / distance between consecutive values\n", + " q_x = (2**n - 1) / (max_x - min_x)\n", + " \n", + " # zp = what should be added to 0 to get min_x -> min_x = (0 + zp) / q -> zp = min_x * q\n", + " zp_x = int(round(min_x * q_x))\n", + " \n", + " # x = (x_q + zp) / q -> x_q = (x * q) - zp\n", + " x_q = ((q_x * x) - zp_x).round().astype(np.uint)\n", + "\n", + " return QuantizedArray(x_q, QuantizationParameters(q_x, zp_x, n))\n", + "\n", + " def dequantize(self):\n", + " # x = (x_q + zp) / q\n", + " # x = (x_q + zp) / q\n", + " return (self.values.astype(np.float32) + float(self.parameters.zp)) / self.parameters.q\n", + "\n", + " def affine(self, w, b, min_y, max_y, n_y):\n", + " # the formulas used in this method was derived from the following equations\n", + " #\n", + " # x = (x_q + zp_x) / q_x\n", + " # w = (w_q + zp_w) / q_w\n", + " # b = (b_q + zp_b) / q_b\n", + " #\n", + " # (x * w) + b = ((x_q + zp_x) / q_x) * ((w_q + zp_w) / q_w) + ((b_q + zp_b) / q_b)\n", + " # = y = (y_q + zp_y) / q_y\n", + " #\n", + " # So, ((x_q + zp_x) / q_x) * ((w_q + zp_w) / q_w) + ((b_q + zp_b) / q_b) = (y_q + zp_y) / q_y\n", + " # We can calculate zp_y and q_y from min_y, max_y, n_y. So, the only unknown is y_q and it can be solved.\n", + "\n", + " x_q = self.values\n", + " w_q = w.values\n", + " b_q = b.values\n", + "\n", + " q_x = self.parameters.q\n", + " q_w = w.parameters.q\n", + " q_b = b.parameters.q\n", + "\n", + " zp_x = self.parameters.zp\n", + " zp_w = w.parameters.zp\n", + " zp_b = b.parameters.zp\n", + "\n", + " q_y = (2**n_y - 1) / (max_y - min_y)\n", + " zp_y = int(round(min_y * q_y))\n", + "\n", + " y_q = (q_y / (q_x * q_w)) * ((x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b))\n", + " y_q -= min_y * q_y\n", + " y_q = y_q.round().clip(0, 2**n_y - 1).astype(np.uint)\n", + "\n", + " return QuantizedArray(y_q, QuantizationParameters(q_y, zp_y, n_y))\n", + "\n", + "class QuantizedFunction:\n", + " def __init__(self, table, input_parameters=None, output_parameters=None):\n", + " self.table = table\n", + " self.input_parameters = input_parameters\n", + " self.output_parameters = output_parameters\n", + "\n", + " @staticmethod\n", + " def of(f, input_bits, output_bits):\n", + " domain = np.array(range(2**input_bits), dtype=np.uint)\n", + " table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", + " return QuantizedFunction(table)\n", + "\n", + " @staticmethod\n", + " def plain(f, input_parameters, output_bits):\n", + " n = input_parameters.n\n", + "\n", + " domain = np.array(range(2**n), dtype=np.uint)\n", + " inputs = QuantizedArray(domain, input_parameters).dequantize()\n", + "\n", + " outputs = f(inputs)\n", + " quantized_outputs = QuantizedArray.of(outputs, output_bits)\n", + "\n", + " table = quantized_outputs.values\n", + " output_parameters = quantized_outputs.parameters\n", + "\n", + " return QuantizedFunction(table, input_parameters, output_parameters)\n", + "\n", + " def apply(self, x):\n", + " assert x.parameters == self.input_parameters\n", + " return QuantizedArray(self.table[x.values], self.output_parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "e5be0800", + "metadata": {}, + "source": [ + "### Let's quantize our model parameters\n", + "\n", + "Since the parameters only consist of scalars, we can use a single bit quantization." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3ec0ad9b", + "metadata": {}, + "outputs": [], + "source": [ + "parameter_bits = 1\n", + "\n", + "w_q = QuantizedArray.of(w, parameter_bits)\n", + "b_q = QuantizedArray.of(b, parameter_bits)" + ] + }, + { + "cell_type": "markdown", + "id": "b43c0371", + "metadata": {}, + "source": [ + "### And quantize our inputs" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "20cea447", + "metadata": {}, + "outputs": [], + "source": [ + "input_bits = 5\n", + "\n", + "x = inputs\n", + "x_q = QuantizedArray.of(inputs, input_bits)" + ] + }, + { + "cell_type": "markdown", + "id": "ca76b68d", + "metadata": {}, + "source": [ + "### Time to make quantized inference" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "8728e939", + "metadata": {}, + "outputs": [], + "source": [ + "output_bits = 7\n", + "\n", + "intermediate = x @ w + b\n", + "intermediate_q = x_q.affine(w_q, b_q, intermediate.min(), intermediate.max(), output_bits)\n", + "\n", + "sigmoid = QuantizedFunction.plain(lambda x: 1 / (1 + np.exp(-x)), intermediate_q.parameters, output_bits)\n", + "y_q = sigmoid.apply(intermediate_q)\n", + "\n", + "quantized_predictions = y_q.dequantize()" + ] + }, + { + "cell_type": "markdown", + "id": "ab782b4a", + "metadata": {}, + "source": [ + "### And visualize the results" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "9d2bb5da", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for column in contour.collections:\n", + " plt.gca().collections.remove(column)\n", + " \n", + "contour = ax.contourf(\n", + " contour_plot_x_data,\n", + " contour_plot_y_data,\n", + " quantized_predictions.round().reshape(contour_plot_x_data.shape),\n", + " cmap=\"gray\",\n", + " alpha=0.50,\n", + ")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "4834cdfc", + "metadata": {}, + "source": [ + "### Now it's time to make the inference homomorphic" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "fcf4ea26", + "metadata": {}, + "outputs": [], + "source": [ + "q_y = (2**output_bits - 1) / (intermediate.max() - intermediate.min())\n", + "zp_y = int(round(intermediate.min() * q_y))\n", + "\n", + "q_x = x_q.parameters.q\n", + "q_w = w_q.parameters.q\n", + "q_b = b_q.parameters.q\n", + "\n", + "zp_x = x_q.parameters.zp\n", + "zp_w = w_q.parameters.zp\n", + "zp_b = b_q.parameters.zp\n", + "\n", + "x_q = x_q.values\n", + "w_q = w_q.values\n", + "b_q = b_q.values" + ] + }, + { + "cell_type": "markdown", + "id": "43e47369", + "metadata": {}, + "source": [ + "### Simplification to rescue!\n", + "\n", + "The `y_q` formula in `QuantizedArray.affine(...)` can be rewritten to make it easier to implement in homomorphically. Here is the breakdown.\n", + "```\n", + "(q_y / (q_x * q_w)) * ((x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b)) - (min_y * q_y)\n", + "^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^\n", + "constant (c1) can be done constant (c2) constant (c3) constant (c4)\n", + " on the circuit \n", + " \n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " can be done on the circuit\n", + " \n", + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "2de0cf20", + "metadata": {}, + "outputs": [], + "source": [ + "c1 = q_y / (q_x * q_w)\n", + "c2 = w_q + zp_w\n", + "c3 = (q_x * q_w / q_b) * (b_q + zp_b)\n", + "c4 = intermediate.min() * q_y\n", + "\n", + "def f(x):\n", + " values = ((c1 * (x + c3)) - c4).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", + " after_affine_q = QuantizedArray(values, intermediate_q.parameters)\n", + " \n", + " sigmoid = QuantizedFunction.plain(lambda x: 1 / (1 + np.exp(-x)), after_affine_q.parameters, output_bits)\n", + " y_q = sigmoid.apply(after_affine_q)\n", + " \n", + " return y_q.values\n", + "\n", + "f_q = QuantizedFunction.of(f, output_bits, output_bits)\n", + "\n", + "from hdk.common.extensions.table import LookupTable\n", + "table = LookupTable([int(entry) for entry in f_q.table])\n", + "\n", + "w_0 = int(c2.flatten()[0])\n", + "w_1 = int(c2.flatten()[1])\n", + "\n", + "def infer(x_0, x_1):\n", + " return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)]" + ] + }, + { + "cell_type": "markdown", + "id": "93eb9499", + "metadata": {}, + "source": [ + "### Time to compile our quantized inference function" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "a80895fd", + "metadata": {}, + "outputs": [], + "source": [ + "from hdk.common.data_types.integers import Integer\n", + "from hdk.common.data_types.values import EncryptedValue\n", + "from hdk.hnumpy.compile import compile_numpy_function\n", + "\n", + "dataset = []\n", + "for x_i in x_q:\n", + " dataset.append((int(x_i[0]), int(x_i[1])))\n", + " \n", + "homomorphic_model = compile_numpy_function(\n", + " infer,\n", + " {\n", + " \"x_0\": EncryptedValue(Integer(input_bits, is_signed=False)),\n", + " \"x_1\": EncryptedValue(Integer(input_bits, is_signed=False)),\n", + " },\n", + " iter(dataset),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f0b08a0f", + "metadata": {}, + "source": [ + "### Here is the textual representation of the operation graph" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2cc4e11d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "%0 = ConstantInput(2) # Integer\n", + "%1 = ConstantInput(1) # Integer\n", + "%2 = x_0 # Integer\n", + "%3 = ConstantInput(6) # Integer\n", + "%4 = x_1 # Integer\n", + "%5 = ConstantInput(6) # Integer\n", + "%6 = Add(2, 3) # Integer\n", + "%7 = Add(4, 5) # Integer\n", + "%8 = Mul(6, 0) # Integer\n", + "%9 = Mul(7, 1) # Integer\n", + "%10 = Add(8, 9) # Integer\n", + "%11 = ArbitraryFunction(10) # Integer\n", + "return(%11)\n" + ] + } + ], + "source": [ + "from hdk.common.debugging import get_printable_graph\n", + "print(get_printable_graph(homomorphic_model, show_data_types=True))" + ] + }, + { + "cell_type": "markdown", + "id": "ade14f17", + "metadata": {}, + "source": [ + "### Finally, it's time to make homomorphic inference\n", + "\n", + "Or, at least, simulate it until the compiler integration is complete." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "dd2d03d7", + "metadata": {}, + "outputs": [], + "source": [ + "homomorphic_predictions = []\n", + "for x_0, x_1 in map(lambda x_i: (int(x_i[0]), int(x_i[1])), x_q):\n", + " evaluation = homomorphic_model.evaluate({0: x_0, 1: x_1})\n", + " inference = QuantizedArray(evaluation[homomorphic_model.output_nodes[0]], y_q.parameters)\n", + " homomorphic_predictions.append(inference.dequantize())\n", + "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" + ] + }, + { + "cell_type": "markdown", + "id": "443fbc03", + "metadata": {}, + "source": [ + "### And visualize it" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "57050b5d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for column in contour.collections:\n", + " plt.gca().collections.remove(column)\n", + " \n", + "contour = ax.contourf(\n", + " contour_plot_x_data,\n", + " contour_plot_y_data,\n", + " homomorphic_predictions.round().reshape(contour_plot_x_data.shape),\n", + " cmap=\"gray\",\n", + " alpha=0.50,\n", + ")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "53ecca94", + "metadata": {}, + "source": [ + "### Enjoy!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/figures/QuantizationVisualized.svg b/examples/figures/QuantizationVisualized.svg new file mode 100644 index 000000000..78da7838b --- /dev/null +++ b/examples/figures/QuantizationVisualized.svg @@ -0,0 +1,3 @@ + + +
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/poetry.lock b/poetry.lock index ef3c23ba3..74a018f89 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,9 +14,34 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "appnope" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "argon2-cffi" +version = "20.1.0" +description = "The secure Argon2 password hashing algorithm." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +cffi = ">=1.0.0" +six = "*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"] +docs = ["sphinx"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] + [[package]] name = "astroid" -version = "2.6.5" +version = "2.6.6" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -28,6 +53,14 @@ typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpyth typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" +[[package]] +name = "async-generator" +version = "1.10" +description = "Async generators and context managers for Python 3.5+" +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "atomicwrites" version = "1.4.0" @@ -61,6 +94,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pytz = ">=2015.7" +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "black" version = "21.7b0" @@ -85,6 +126,19 @@ d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bleach" +version = "4.0.0" +description = "An easy safelist-based HTML-sanitizing tool." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + [[package]] name = "certifi" version = "2021.5.30" @@ -93,6 +147,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "cffi" +version = "1.14.6" +description = "Foreign Function Interface for Python calling C code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "chardet" version = "4.0.0" @@ -155,19 +220,38 @@ python-versions = "*" six = "*" [[package]] -name = "diff-cover" -version = "6.2.1" -description = "Automatically find diff lines that need test coverage." +name = "decorator" +version = "5.0.9" +description = "Decorators for Humans" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.5" + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "diff-cover" +version = "6.3.0" +description = "Run coverage and linting reports on diffs" +category = "dev" +optional = false +python-versions = ">=3.6.2,<4.0.0" [package.dependencies] chardet = ">=3.0.0" Jinja2 = ">=2.7.1" -jinja2-pluralize = "*" -pluggy = "*" -pygments = "*" +jinja2_pluralize = ">=0.3.0,<0.4.0" +pluggy = ">=0.13.1,<0.14.0" +Pygments = ">=2.9.0,<3.0.0" + +[package.extras] +toml = ["tomli (>=1.2.1,<2.0.0)"] [[package]] name = "docutils" @@ -177,6 +261,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "entrypoints" +version = "0.3" +description = "Discover and load entry points from installed packages." +category = "dev" +optional = false +python-versions = ">=2.7" + [[package]] name = "idna" version = "3.2" @@ -230,6 +322,83 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "ipykernel" +version = "5.5.5" +description = "IPython Kernel for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +ipython = ">=5.0.0" +jupyter-client = "*" +tornado = ">=4.2" +traitlets = ">=4.1.0" + +[package.extras] +test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "jedi (<=0.17.2)"] + +[[package]] +name = "ipython" +version = "7.26.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "ipywidgets" +version = "7.6.3" +description = "IPython HTML widgets for Jupyter" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ipykernel = ">=4.5.1" +ipython = {version = ">=4.0.0", markers = "python_version >= \"3.3\""} +jupyterlab-widgets = {version = ">=1.0.0", markers = "python_version >= \"3.6\""} +nbformat = ">=4.2.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=3.5.0,<3.6.0" + +[package.extras] +test = ["pytest (>=3.6.0)", "pytest-cov", "mock"] + [[package]] name = "isort" version = "5.9.3" @@ -244,6 +413,21 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "jedi" +version = "0.18.0" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] + [[package]] name = "jinja2" version = "3.0.1" @@ -270,6 +454,109 @@ python-versions = "*" inflect = ">=0.2.4" jinja2 = ">=2.4" +[[package]] +name = "jsonschema" +version = "3.2.0" +description = "An implementation of JSON Schema validation for Python" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +attrs = ">=17.4.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +pyrsistent = ">=0.14.0" +six = ">=1.11.0" + +[package.extras] +format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] +format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] + +[[package]] +name = "jupyter" +version = "1.0.0" +description = "Jupyter metapackage. Install all the Jupyter components in one go." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ipykernel = "*" +ipywidgets = "*" +jupyter-console = "*" +nbconvert = "*" +notebook = "*" +qtconsole = "*" + +[[package]] +name = "jupyter-client" +version = "6.2.0" +description = "Jupyter protocol implementation and client libraries" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +jupyter-core = ">=4.6.0" +nest-asyncio = ">=1.5" +python-dateutil = ">=2.1" +pyzmq = ">=13" +tornado = ">=4.1" +traitlets = "*" + +[package.extras] +doc = ["sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +test = ["async-generator", "ipykernel", "ipython", "mock", "pytest-asyncio", "pytest-timeout", "pytest", "mypy", "pre-commit", "jedi (<0.18)"] + +[[package]] +name = "jupyter-console" +version = "6.4.0" +description = "Jupyter terminal console" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +ipykernel = "*" +ipython = "*" +jupyter-client = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" + +[package.extras] +test = ["pexpect"] + +[[package]] +name = "jupyter-core" +version = "4.7.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\""} +traitlets = "*" + +[[package]] +name = "jupyterlab-pygments" +version = "0.1.2" +description = "Pygments theme using JupyterLab CSS variables" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pygments = ">=2.4.1,<3" + +[[package]] +name = "jupyterlab-widgets" +version = "1.0.0" +description = "A JupyterLab extension." +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "kiwisolver" version = "1.3.1" @@ -316,7 +603,7 @@ python-versions = ">=3.6" [[package]] name = "matplotlib" -version = "3.4.2" +version = "3.4.3" description = "Python plotting package" category = "main" optional = false @@ -330,6 +617,17 @@ pillow = ">=6.2.0" pyparsing = ">=2.2.1" python-dateutil = ">=2.7" +[[package]] +name = "matplotlib-inline" +version = "0.1.2" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" + [[package]] name = "mccabe" version = "0.6.1" @@ -354,6 +652,14 @@ code_style = ["pre-commit (==2.6)"] rtd = ["myst-parser (==0.14.0a3)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] testing = ["coverage", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"] +[[package]] +name = "mistune" +version = "0.8.4" +description = "The fastest markdown parser in pure Python" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "mypy" version = "0.910" @@ -402,6 +708,99 @@ linkify = ["linkify-it-py (>=1.0,<2.0)"] rtd = ["ipython", "sphinx-book-theme (>=0.1.0,<0.2.0)", "sphinx-panels (>=0.5.2,<0.6.0)", "sphinxcontrib-bibtex (>=2.1,<3.0)", "sphinxext-rediraffe (>=0.2,<1.0)", "sphinxcontrib.mermaid (>=0.6.3,<0.7.0)", "sphinxext-opengraph (>=0.4.2,<0.5.0)"] testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest (>=3.6,<4)", "pytest-cov", "pytest-regressions"] +[[package]] +name = "nbclient" +version = "0.5.3" +description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +async-generator = "*" +jupyter-client = ">=6.1.5" +nbformat = ">=5.0" +nest-asyncio = "*" +traitlets = ">=4.2" + +[package.extras] +dev = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "tox", "bumpversion", "xmltodict", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)", "black"] +sphinx = ["Sphinx (>=1.7)", "sphinx-book-theme", "mock", "moto", "myst-parser"] +test = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>=4.1)", "pytest-cov (>=2.6.1)", "check-manifest", "flake8", "mypy", "tox", "bumpversion", "xmltodict", "pip (>=18.1)", "wheel (>=0.31.0)", "setuptools (>=38.6.0)", "twine (>=1.11.0)", "black"] + +[[package]] +name = "nbconvert" +version = "6.1.0" +description = "Converting Jupyter Notebooks" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +bleach = "*" +defusedxml = "*" +entrypoints = ">=0.2.2" +jinja2 = ">=2.4" +jupyter-core = "*" +jupyterlab-pygments = "*" +mistune = ">=0.8.1,<2" +nbclient = ">=0.5.0,<0.6.0" +nbformat = ">=4.4" +pandocfilters = ">=1.4.1" +pygments = ">=2.4.1" +testpath = "*" +traitlets = ">=5.0" + +[package.extras] +all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +serve = ["tornado (>=4.0)"] +test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)"] +webpdf = ["pyppeteer (==0.2.2)"] + +[[package]] +name = "nbformat" +version = "5.1.3" +description = "The Jupyter Notebook format" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +ipython-genutils = "*" +jsonschema = ">=2.4,<2.5.0 || >2.5.0" +jupyter-core = "*" +traitlets = ">=4.1" + +[package.extras] +fast = ["fastjsonschema"] +test = ["check-manifest", "fastjsonschema", "testpath", "pytest", "pytest-cov"] + +[[package]] +name = "nbmake" +version = "0.5" +description = "Pytest plugin for testing notebooks" +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +ipykernel = ">=5.4.0,<6.0.0" +nbclient = ">=0.3,<1.0" +nbformat = ">=5.0.8,<6.0.0" +pathlib = ">=1.0.1,<2.0.0" +pydantic = ">=1.7.2,<2.0.0" +Pygments = ">=2.7.3,<3.0.0" +pytest = ">=6.1.2,<7.0.0" + +[[package]] +name = "nest-asyncio" +version = "1.5.1" +description = "Patch asyncio to allow nested event loops" +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "networkx" version = "2.6.2" @@ -417,6 +816,35 @@ doc = ["sphinx (>=4.0,<5.0)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx-galler extra = ["lxml (>=4.5)", "pygraphviz (>=1.7)", "pydot (>=1.4.1)"] test = ["pytest (>=6.2)", "pytest-cov (>=2.12)", "codecov (>=2.1)"] +[[package]] +name = "notebook" +version = "6.4.3" +description = "A web-based notebook environment for interactive computing" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +argon2-cffi = "*" +ipykernel = "*" +ipython-genutils = "*" +jinja2 = "*" +jupyter-client = ">=5.3.4" +jupyter-core = ">=4.6.1" +nbconvert = "*" +nbformat = "*" +prometheus-client = "*" +pyzmq = ">=17" +Send2Trash = ">=1.5.0" +terminado = ">=0.8.3" +tornado = ">=6.1" +traitlets = ">=4.2.1" + +[package.extras] +docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"] +json-logging = ["json-logging"] +test = ["pytest", "coverage", "requests", "nbval", "selenium", "pytest-cov", "requests-unixsocket"] + [[package]] name = "numpy" version = "1.21.1" @@ -436,6 +864,34 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" +[[package]] +name = "pandocfilters" +version = "1.4.3" +description = "Utilities for writing pandoc filters in python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "parso" +version = "0.8.2" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pathlib" +version = "1.0.1" +description = "Object-oriented filesystem paths" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pathspec" version = "0.9.0" @@ -444,6 +900,25 @@ category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pillow" version = "8.3.1" @@ -466,6 +941,36 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "prometheus-client" +version = "0.11.0" +description = "Python client for the Prometheus monitoring system." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.19" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "py" version = "1.10.0" @@ -474,6 +979,29 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pydocstyle" version = "6.1.1" @@ -519,6 +1047,14 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "pyrsistent" +version = "0.18.0" +description = "Persistent/Functional/Immutable data structures" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "pytest" version = "6.2.4" @@ -576,6 +1112,22 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32" +version = "301" +description = "Python for Window Extensions" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pywinpty" +version = "1.1.3" +description = "Pseudo terminal support for Windows from Python." +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "pyyaml" version = "5.4.1" @@ -584,9 +1136,51 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +[[package]] +name = "pyzmq" +version = "22.2.1" +description = "Python bindings for 0MQ" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} +py = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "qtconsole" +version = "5.1.1" +description = "Jupyter Qt console" +category = "dev" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +ipykernel = ">=4.1" +ipython-genutils = "*" +jupyter-client = ">=4.1" +jupyter-core = "*" +pygments = "*" +pyzmq = ">=17.1" +qtpy = "*" +traitlets = "*" + +[package.extras] +doc = ["Sphinx (>=1.3)"] +test = ["flaky", "pytest", "pytest-qt"] + +[[package]] +name = "qtpy" +version = "1.9.0" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "regex" -version = "2021.7.6" +version = "2021.8.3" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -610,6 +1204,19 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "send2trash" +version = "1.8.0" +description = "Send file to trash natively under Mac OS X, Windows and Linux." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +nativelib = ["pyobjc-framework-cocoa", "pywin32"] +objc = ["pyobjc-framework-cocoa"] +win32 = ["pywin32"] + [[package]] name = "six" version = "1.16.0" @@ -743,6 +1350,33 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "terminado" +version = "0.11.0" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "testpath" +version = "0.5.0" +description = "Test utilities for code working with files and commands" +category = "dev" +optional = false +python-versions = ">= 3.5" + +[package.extras] +test = ["pytest", "pathlib2"] + [[package]] name = "toml" version = "0.10.2" @@ -753,12 +1387,34 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.0" +version = "1.2.1" description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "tornado" +version = "6.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "dev" +optional = false +python-versions = ">= 3.5" + +[[package]] +name = "traitlets" +version = "5.0.5" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +ipython-genutils = "*" + +[package.extras] +test = ["pytest"] + [[package]] name = "typed-ast" version = "1.4.3" @@ -788,6 +1444,33 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "widgetsnbextension" +version = "3.5.1" +description = "IPython HTML widgets for Jupyter" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +notebook = ">=4.4.1" + [[package]] name = "wrapt" version = "1.12.1" @@ -811,7 +1494,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "e5b217fd28a7ed316b4bcc779ffb6616596b4b596293fc1e87cda7dceeccc7f8" +content-hash = "1c23e8e0262499b55c7652407e635835efe43581d63693d668c6f2ed812d5f56" [metadata.files] alabaster = [ @@ -822,9 +1505,41 @@ appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +argon2-cffi = [ + {file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-win32.whl", hash = "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5"}, + {file = "argon2_cffi-20.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc"}, + {file = "argon2_cffi-20.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe"}, + {file = "argon2_cffi-20.1.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647"}, + {file = "argon2_cffi-20.1.0-cp35-cp35m-win32.whl", hash = "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361"}, + {file = "argon2_cffi-20.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b"}, + {file = "argon2_cffi-20.1.0-cp36-cp36m-win32.whl", hash = "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496"}, + {file = "argon2_cffi-20.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa"}, + {file = "argon2_cffi-20.1.0-cp37-abi3-macosx_10_6_intel.whl", hash = "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b"}, + {file = "argon2_cffi-20.1.0-cp37-cp37m-win32.whl", hash = "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5"}, + {file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"}, + {file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"}, + {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"}, + {file = "argon2_cffi-20.1.0-cp39-cp39-win32.whl", hash = "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be"}, + {file = "argon2_cffi-20.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32"}, + {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b94042e5dcaa5d08cf104a54bfae614be502c6f44c9c89ad1535b2ebdaacbd4c"}, + {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:8282b84ceb46b5b75c3a882b28856b8cd7e647ac71995e71b6705ec06fc232c3"}, + {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3aa804c0e52f208973845e8b10c70d8957c9e5a666f702793256242e9167c4e0"}, + {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:36320372133a003374ef4275fbfce78b7ab581440dfca9f9471be3dd9a522428"}, +] astroid = [ - {file = "astroid-2.6.5-py3-none-any.whl", hash = "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925"}, - {file = "astroid-2.6.5.tar.gz", hash = "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f"}, + {file = "astroid-2.6.6-py3-none-any.whl", hash = "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef"}, + {file = "astroid-2.6.6.tar.gz", hash = "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334"}, +] +async-generator = [ + {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, + {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -838,14 +1553,69 @@ babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] black = [ {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, ] +bleach = [ + {file = "bleach-4.0.0-py2.py3-none-any.whl", hash = "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d"}, + {file = "bleach-4.0.0.tar.gz", hash = "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8"}, +] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] +cffi = [ + {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, + {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, + {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, + {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, + {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, + {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, + {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, + {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, + {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, + {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, + {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, + {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, + {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, + {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, + {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, + {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, + {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, + {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, + {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, + {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, + {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, + {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, + {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, + {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, + {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, + {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, +] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, @@ -920,14 +1690,26 @@ cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] +decorator = [ + {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, + {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, +] +defusedxml = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] diff-cover = [ - {file = "diff_cover-6.2.1-py3-none-any.whl", hash = "sha256:cfe6333c9b83a28f91214e06e3a86108aeeb6a9a31233944ce8ef236121ba899"}, - {file = "diff_cover-6.2.1.tar.gz", hash = "sha256:b22fe97d118fadb2528bbc0a1f9fc370491b29b1b3fe2e2f1cdb94bf028814c7"}, + {file = "diff_cover-6.3.0-py3-none-any.whl", hash = "sha256:105b82e897a0a594ef5928a745a23edb06ff213bcb4d31644967fad9576e1b96"}, + {file = "diff_cover-6.3.0.tar.gz", hash = "sha256:f55255a8855c6d2f58b1f9cd967bf27cd5c84fcc1630b785d405625aa17be99f"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, @@ -948,10 +1730,30 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +ipykernel = [ + {file = "ipykernel-5.5.5-py3-none-any.whl", hash = "sha256:29eee66548ee7c2edb7941de60c0ccf0a7a8dd957341db0a49c5e8e6a0fcb712"}, + {file = "ipykernel-5.5.5.tar.gz", hash = "sha256:e976751336b51082a89fc2099fb7f96ef20f535837c398df6eab1283c2070884"}, +] +ipython = [ + {file = "ipython-7.26.0-py3-none-any.whl", hash = "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362"}, + {file = "ipython-7.26.0.tar.gz", hash = "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +ipywidgets = [ + {file = "ipywidgets-7.6.3-py2.py3-none-any.whl", hash = "sha256:e6513cfdaf5878de30f32d57f6dc2474da395a2a2991b94d487406c0ab7f55ca"}, + {file = "ipywidgets-7.6.3.tar.gz", hash = "sha256:9f1a43e620530f9e570e4a493677d25f08310118d315b00e25a18f12913c41f0"}, +] isort = [ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] +jedi = [ + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, +] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, @@ -960,6 +1762,35 @@ jinja2-pluralize = [ {file = "jinja2_pluralize-0.3.0-py2.py3-none-any.whl", hash = "sha256:4fec874a591014774d4c66cb7f65314390731bfc57db4c27119db61aa93b2bc4"}, {file = "jinja2_pluralize-0.3.0.tar.gz", hash = "sha256:df5c2d5017b9b54c0a66cb790cca9fc08945837c3dbfc323589203f1ffb73c1c"}, ] +jsonschema = [ + {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, + {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, +] +jupyter = [ + {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, + {file = "jupyter-1.0.0.tar.gz", hash = "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"}, + {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, +] +jupyter-client = [ + {file = "jupyter_client-6.2.0-py3-none-any.whl", hash = "sha256:9715152067e3f7ea3b56f341c9a0f9715c8c7cc316ee0eb13c3c84f5ca0065f5"}, + {file = "jupyter_client-6.2.0.tar.gz", hash = "sha256:e2ab61d79fbf8b56734a4c2499f19830fbd7f6fefb3e87868ef0545cb3c17eb9"}, +] +jupyter-console = [ + {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, + {file = "jupyter_console-6.4.0.tar.gz", hash = "sha256:242248e1685039cd8bff2c2ecb7ce6c1546eb50ee3b08519729e6e881aec19c7"}, +] +jupyter-core = [ + {file = "jupyter_core-4.7.1-py3-none-any.whl", hash = "sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e"}, + {file = "jupyter_core-4.7.1.tar.gz", hash = "sha256:79025cb3225efcd36847d0840f3fc672c0abd7afd0de83ba8a1d3837619122b4"}, +] +jupyterlab-pygments = [ + {file = "jupyterlab_pygments-0.1.2-py2.py3-none-any.whl", hash = "sha256:abfb880fd1561987efaefcb2d2ac75145d2a5d0139b1876d5be806e32f630008"}, + {file = "jupyterlab_pygments-0.1.2.tar.gz", hash = "sha256:cfcda0873626150932f438eccf0f8bf22bfa92345b814890ab360d666b254146"}, +] +jupyterlab-widgets = [ + {file = "jupyterlab_widgets-1.0.0-py3-none-any.whl", hash = "sha256:caeaf3e6103180e654e7d8d2b81b7d645e59e432487c1d35a41d6d3ee56b3fef"}, + {file = "jupyterlab_widgets-1.0.0.tar.gz", hash = "sha256:5c1a29a84d3069208cb506b10609175b249b6486d6b1cbae8fcde2a11584fb78"}, +] kiwisolver = [ {file = "kiwisolver-1.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"}, {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0"}, @@ -1023,12 +1854,22 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1037,14 +1878,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1054,30 +1902,39 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] matplotlib = [ - {file = "matplotlib-3.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c541ee5a3287efe066bbe358320853cf4916bc14c00c38f8f3d8d75275a405a9"}, - {file = "matplotlib-3.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3a5c18dbd2c7c366da26a4ad1462fe3e03a577b39e3b503bbcf482b9cdac093c"}, - {file = "matplotlib-3.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a9d8cb5329df13e0cdaa14b3b43f47b5e593ec637f13f14db75bb16e46178b05"}, - {file = "matplotlib-3.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:7ad19f3fb6145b9eb41c08e7cbb9f8e10b91291396bee21e9ce761bb78df63ec"}, - {file = "matplotlib-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:7a58f3d8fe8fac3be522c79d921c9b86e090a59637cb88e3bc51298d7a2c862a"}, - {file = "matplotlib-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6382bc6e2d7e481bcd977eb131c31dee96e0fb4f9177d15ec6fb976d3b9ace1a"}, - {file = "matplotlib-3.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a6a44f27aabe720ec4fd485061e8a35784c2b9ffa6363ad546316dfc9cea04e"}, - {file = "matplotlib-3.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1c1779f7ab7d8bdb7d4c605e6ffaa0614b3e80f1e3c8ccf7b9269a22dbc5986b"}, - {file = "matplotlib-3.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5826f56055b9b1c80fef82e326097e34dc4af8c7249226b7dd63095a686177d1"}, - {file = "matplotlib-3.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:0bea5ec5c28d49020e5d7923c2725b837e60bc8be99d3164af410eb4b4c827da"}, - {file = "matplotlib-3.4.2-cp38-cp38-win32.whl", hash = "sha256:6475d0209024a77f869163ec3657c47fed35d9b6ed8bccba8aa0f0099fbbdaa8"}, - {file = "matplotlib-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:21b31057bbc5e75b08e70a43cefc4c0b2c2f1b1a850f4a0f7af044eb4163086c"}, - {file = "matplotlib-3.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b26535b9de85326e6958cdef720ecd10bcf74a3f4371bf9a7e5b2e659c17e153"}, - {file = "matplotlib-3.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:32fa638cc10886885d1ca3d409d4473d6a22f7ceecd11322150961a70fab66dd"}, - {file = "matplotlib-3.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:956c8849b134b4a343598305a3ca1bdd3094f01f5efc8afccdebeffe6b315247"}, - {file = "matplotlib-3.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:85f191bb03cb1a7b04b5c2cca4792bef94df06ef473bc49e2818105671766fee"}, - {file = "matplotlib-3.4.2-cp39-cp39-win32.whl", hash = "sha256:b1d5a2cedf5de05567c441b3a8c2651fbde56df08b82640e7f06c8cd91e201f6"}, - {file = "matplotlib-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:df815378a754a7edd4559f8c51fc7064f779a74013644a7f5ac7a0c31f875866"}, - {file = "matplotlib-3.4.2.tar.gz", hash = "sha256:d8d994cefdff9aaba45166eb3de4f5211adb4accac85cbf97137e98f26ea0219"}, + {file = "matplotlib-3.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c988bb43414c7c2b0a31bd5187b4d27fd625c080371b463a6d422047df78913"}, + {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1c5efc278d996af8a251b2ce0b07bbeccb821f25c8c9846bdcb00ffc7f158aa"}, + {file = "matplotlib-3.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eeb1859efe7754b1460e1d4991bbd4a60a56f366bc422ef3a9c5ae05f0bc70b5"}, + {file = "matplotlib-3.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:844a7b0233e4ff7fba57e90b8799edaa40b9e31e300b8d5efc350937fa8b1bea"}, + {file = "matplotlib-3.4.3-cp37-cp37m-win32.whl", hash = "sha256:85f0c9cf724715e75243a7b3087cf4a3de056b55e05d4d76cc58d610d62894f3"}, + {file = "matplotlib-3.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c70b6311dda3e27672f1bf48851a0de816d1ca6aaf3d49365fbdd8e959b33d2b"}, + {file = "matplotlib-3.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b884715a59fec9ad3b6048ecf3860f3b2ce965e676ef52593d6fa29abcf7d330"}, + {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a78a3b51f29448c7f4d4575e561f6b0dbb8d01c13c2046ab6c5220eb25c06506"}, + {file = "matplotlib-3.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6a724e3a48a54b8b6e7c4ae38cd3d07084508fa47c410c8757e9db9791421838"}, + {file = "matplotlib-3.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48e1e0859b54d5f2e29bb78ca179fd59b971c6ceb29977fb52735bfd280eb0f5"}, + {file = "matplotlib-3.4.3-cp38-cp38-win32.whl", hash = "sha256:01c9de93a2ca0d128c9064f23709362e7fefb34910c7c9e0b8ab0de8258d5eda"}, + {file = "matplotlib-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:ebfb01a65c3f5d53a8c2a8133fec2b5221281c053d944ae81ff5822a68266617"}, + {file = "matplotlib-3.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b53f336a4688cfce615887505d7e41fd79b3594bf21dd300531a4f5b4f746a"}, + {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:fcd6f1954943c0c192bfbebbac263f839d7055409f1173f80d8b11a224d236da"}, + {file = "matplotlib-3.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6be8df61b1626e1a142c57e065405e869e9429b4a6dab4a324757d0dc4d42235"}, + {file = "matplotlib-3.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:41b6e307458988891fcdea2d8ecf84a8c92d53f84190aa32da65f9505546e684"}, + {file = "matplotlib-3.4.3-cp39-cp39-win32.whl", hash = "sha256:f72657f1596199dc1e4e7a10f52a4784ead8a711f4e5b59bea95bdb97cf0e4fd"}, + {file = "matplotlib-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:f15edcb0629a0801738925fe27070480f446fcaa15de65946ff946ad99a59a40"}, + {file = "matplotlib-3.4.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:556965514b259204637c360d213de28d43a1f4aed1eca15596ce83f768c5a56f"}, + {file = "matplotlib-3.4.3-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:54a026055d5f8614f184e588f6e29064019a0aa8448450214c0b60926d62d919"}, + {file = "matplotlib-3.4.3.tar.gz", hash = "sha256:fc4f526dfdb31c9bd6b8ca06bf9fab663ca12f3ec9cdf4496fb44bc680140318"}, +] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, + {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -1087,6 +1944,10 @@ mdit-py-plugins = [ {file = "mdit-py-plugins-0.2.8.tar.gz", hash = "sha256:5991cef645502e80a5388ec4fc20885d2313d4871e8b8e320ca2de14ac0c015f"}, {file = "mdit_py_plugins-0.2.8-py3-none-any.whl", hash = "sha256:1833bf738e038e35d89cb3a07eb0d227ed647ce7dd357579b65343740c6d249c"}, ] +mistune = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] mypy = [ {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, @@ -1120,10 +1981,34 @@ myst-parser = [ {file = "myst-parser-0.15.1.tar.gz", hash = "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603"}, {file = "myst_parser-0.15.1-py3-none-any.whl", hash = "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"}, ] +nbclient = [ + {file = "nbclient-0.5.3-py3-none-any.whl", hash = "sha256:e79437364a2376892b3f46bedbf9b444e5396cfb1bc366a472c37b48e9551500"}, + {file = "nbclient-0.5.3.tar.gz", hash = "sha256:db17271330c68c8c88d46d72349e24c147bb6f34ec82d8481a8f025c4d26589c"}, +] +nbconvert = [ + {file = "nbconvert-6.1.0-py3-none-any.whl", hash = "sha256:37cd92ff2ae6a268e62075ff8b16129e0be4939c4dfcee53dc77cc8a7e06c684"}, + {file = "nbconvert-6.1.0.tar.gz", hash = "sha256:d22a8ff202644d31db254d24d52c3a96c82156623fcd7c7f987bba2612303ec9"}, +] +nbformat = [ + {file = "nbformat-5.1.3-py3-none-any.whl", hash = "sha256:eb8447edd7127d043361bc17f2f5a807626bc8e878c7709a1c647abda28a9171"}, + {file = "nbformat-5.1.3.tar.gz", hash = "sha256:b516788ad70771c6250977c1374fcca6edebe6126fd2adb5a69aa5c2356fd1c8"}, +] +nbmake = [ + {file = "nbmake-0.5-py3-none-any.whl", hash = "sha256:8a0b3ce9ca26320165c6de532c3d36445da1dd53c2c8fac4870ed900b3cbe538"}, + {file = "nbmake-0.5.tar.gz", hash = "sha256:da9bf1bbc377c9d1d697f99952834017c39b4983e7e482a038dec705955a8ae9"}, +] +nest-asyncio = [ + {file = "nest_asyncio-1.5.1-py3-none-any.whl", hash = "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c"}, + {file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"}, +] networkx = [ {file = "networkx-2.6.2-py3-none-any.whl", hash = "sha256:5fcb7004be69e8fbdf07dcb502efa5c77cadcaad6982164134eeb9721f826c2e"}, {file = "networkx-2.6.2.tar.gz", hash = "sha256:2306f1950ce772c5a59a57f5486d59bb9cab98497c45fc49cbc45ac0dec119bb"}, ] +notebook = [ + {file = "notebook-6.4.3-py3-none-any.whl", hash = "sha256:b50eafa8208d5db966efd1caa4076b4dfc51815e02a805b32ecd717e9e6cc071"}, + {file = "notebook-6.4.3.tar.gz", hash = "sha256:e6b6dfed36b00cf950f63c0d42e947c101d4258aec21624de62b9e0c11ed5c0d"}, +] numpy = [ {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, @@ -1158,10 +2043,28 @@ packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] +pandocfilters = [ + {file = "pandocfilters-1.4.3.tar.gz", hash = "sha256:bc63fbb50534b4b1f8ebe1860889289e8af94a23bff7445259592df25a3906eb"}, +] +parso = [ + {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, + {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, +] +pathlib = [ + {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, +] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] pillow = [ {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, @@ -1207,10 +2110,50 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +prometheus-client = [ + {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"}, + {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, + {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pydantic = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, @@ -1227,6 +2170,29 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pyrsistent = [ + {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, + {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, + {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, +] pytest = [ {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, @@ -1243,6 +2209,25 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +pywin32 = [ + {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, + {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, + {file = "pywin32-301-cp36-cp36m-win32.whl", hash = "sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0"}, + {file = "pywin32-301-cp36-cp36m-win_amd64.whl", hash = "sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78"}, + {file = "pywin32-301-cp37-cp37m-win32.whl", hash = "sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b"}, + {file = "pywin32-301-cp37-cp37m-win_amd64.whl", hash = "sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a"}, + {file = "pywin32-301-cp38-cp38-win32.whl", hash = "sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17"}, + {file = "pywin32-301-cp38-cp38-win_amd64.whl", hash = "sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96"}, + {file = "pywin32-301-cp39-cp39-win32.whl", hash = "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe"}, + {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"}, +] +pywinpty = [ + {file = "pywinpty-1.1.3-cp36-none-win_amd64.whl", hash = "sha256:81dc6f16d917b756e06fc58943e9750d59dbefc0ffd2086871d3fa5f33824446"}, + {file = "pywinpty-1.1.3-cp37-none-win_amd64.whl", hash = "sha256:54557887e712ea3215ab0d9f089ed55a6cc8d826cd5d1e340d75300654c9663f"}, + {file = "pywinpty-1.1.3-cp38-none-win_amd64.whl", hash = "sha256:f5e25197397f1fef0362caf3eb89f25441827a1e48bf15827c27021592fd2160"}, + {file = "pywinpty-1.1.3-cp39-none-win_amd64.whl", hash = "sha256:b767276224f86b7560eb9173ba7956758cafcdfab97bb33837d42d2a0f1dbf67"}, + {file = "pywinpty-1.1.3.tar.gz", hash = "sha256:3a1d57b338390333812a5eed31c93c7d8ba82b131078063703e731946d90c9f2"}, +] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -1274,53 +2259,96 @@ pyyaml = [ {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] +pyzmq = [ + {file = "pyzmq-22.2.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:d60a407663b7c2af781ab7f49d94a3d379dd148bb69ea8d9dd5bc69adf18097c"}, + {file = "pyzmq-22.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:631f932fb1fa4b76f31adf976f8056519bc6208a3c24c184581c3dd5be15066e"}, + {file = "pyzmq-22.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0471d634c7fe48ff7d3849798da6c16afc71676dd890b5ae08eb1efe735c6fec"}, + {file = "pyzmq-22.2.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f520e9fee5d7a2e09b051d924f85b977c6b4e224e56c0551c3c241bbeeb0ad8d"}, + {file = "pyzmq-22.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1b6619ceb33a8907f1cb82ff8afc8a133e7a5f16df29528e919734718600426"}, + {file = "pyzmq-22.2.1-cp310-cp310-win32.whl", hash = "sha256:31c5dfb6df5148789835128768c01bf6402eb753d06f524f12f6786caf96fb44"}, + {file = "pyzmq-22.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:4842a8263cbaba6fce401bbe4e2b125321c401a01714e42624dabc554bfc2629"}, + {file = "pyzmq-22.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b921758f8b5098faa85f341bbdd5e36d5339de5e9032ca2b07d8c8e7bec5069b"}, + {file = "pyzmq-22.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:240b83b3a8175b2f616f80092cbb019fcd5c18598f78ffc6aa0ae9034b300f14"}, + {file = "pyzmq-22.2.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:da7f7f3bb08bcf59a6b60b4e53dd8f08bb00c9e61045319d825a906dbb3c8fb7"}, + {file = "pyzmq-22.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e66025b64c4724ba683d6d4a4e5ee23de12fe9ae683908f0c7f0f91b4a2fd94e"}, + {file = "pyzmq-22.2.1-cp36-cp36m-win32.whl", hash = "sha256:50d007d5702171bc810c1e74498fa2c7bc5b50f9750697f7fd2a3e71a25aad91"}, + {file = "pyzmq-22.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b4a51c7d906dc263a0cc5590761e53e0a68f2c2fefe549cbef21c9ee5d2d98a4"}, + {file = "pyzmq-22.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:93705cb90baa9d6f75e8448861a1efd3329006f79095ab18846bd1eaa342f7c3"}, + {file = "pyzmq-22.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:620b0abb813958cb3ecb5144c177e26cde92fee6f43c4b9de6b329515532bf27"}, + {file = "pyzmq-22.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2dd3896b3c952cf6c8013deda53c1df16bf962f355b5503d23521e0f6403ae3d"}, + {file = "pyzmq-22.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e9c030222893afa86881d7485d3e841969760a16004bd23e9a83cca28b42778"}, + {file = "pyzmq-22.2.1-cp37-cp37m-win32.whl", hash = "sha256:262f470e7acde18b7217aac78d19d2e29ced91a5afbeb7d98521ebf26461aa7e"}, + {file = "pyzmq-22.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:246f27b88722cfa729bb04881e94484e40b085720d728c1b05133b3f331b0b7b"}, + {file = "pyzmq-22.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0d17bac19e934e9f547a8811b7c2a32651a7840f38086b924e2e3dcb2fae5c3a"}, + {file = "pyzmq-22.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5933d1f4087de6e52906f72d92e1e4dcc630d371860b92c55d7f7a4b815a664c"}, + {file = "pyzmq-22.2.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac4497e4b7d134ee53ce5532d9cc3b640d6e71806a55062984e0c99a2f88f465"}, + {file = "pyzmq-22.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66375a6094af72a6098ed4403b15b4db6bf00013c6febc1baa832e7abda827f4"}, + {file = "pyzmq-22.2.1-cp38-cp38-win32.whl", hash = "sha256:b2c16d20bd0aef8e57bc9505fdd80ea0d6008020c3740accd96acf1b3d1b5347"}, + {file = "pyzmq-22.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff345d48940c834168f81fa1d4724675099f148f1ab6369748c4d712ed71bf7c"}, + {file = "pyzmq-22.2.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:f5c84c5de9a773bbf8b22c51e28380999ea72e5e85b4db8edf5e69a7a0d4d9f9"}, + {file = "pyzmq-22.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2534a036b777f957bd6b89b55fb2136775ca2659fb0f1c85036ba78d17d86fd5"}, + {file = "pyzmq-22.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a649065413ba4eab92a783a7caa4de8ce14cf46ba8a2a09951426143f1298adb"}, + {file = "pyzmq-22.2.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9cb0bd3a3cb7ccad3caa1d7b0d18ba71ed3a4a3610028e506a4084371d4d223"}, + {file = "pyzmq-22.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4428302c389fffc0c9c07a78cad5376636b9d096f332acfe66b321ae9ff2c63"}, + {file = "pyzmq-22.2.1-cp39-cp39-win32.whl", hash = "sha256:6a5b4566f66d953601d0d47d4071897f550a265bafd52ebcad5ac7aad3838cbb"}, + {file = "pyzmq-22.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:89200ab6ef9081c72a04ed84c52a50b60dcb0655375aeedb40689bc7c934715e"}, + {file = "pyzmq-22.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed67df4eaa99a20d162d76655bda23160abdf8abf82a17f41dfd3962e608dbcc"}, + {file = "pyzmq-22.2.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:021e22a8c58ab294bd4b96448a2ca4e716e1d76600192ff84c33d71edb1fbd37"}, + {file = "pyzmq-22.2.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:200ac096cee5499964c90687306a7244b79ef891f773ed4cf15019fd1f3df330"}, + {file = "pyzmq-22.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b3f57bee62e36be5c97712de32237c5589caee0d1154c2ad01a888accfae20bc"}, + {file = "pyzmq-22.2.1.tar.gz", hash = "sha256:6d18c76676771fd891ca8e0e68da0bbfb88e30129835c0ade748016adb3b6242"}, +] +qtconsole = [ + {file = "qtconsole-5.1.1-py3-none-any.whl", hash = "sha256:73994105b0369bb99f4164df4a131010f3c7b33a7b5169c37366358d8744675b"}, + {file = "qtconsole-5.1.1.tar.gz", hash = "sha256:bbc34bca14f65535afcb401bc74b752bac955e5313001ba640383f7e5857dc49"}, +] +qtpy = [ + {file = "QtPy-1.9.0-py2.py3-none-any.whl", hash = "sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea"}, + {file = "QtPy-1.9.0.tar.gz", hash = "sha256:2db72c44b55d0fe1407be8fba35c838ad0d6d3bb81f23007886dc1fc0f459c8d"}, +] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, + {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, + {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, + {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, + {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, + {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, + {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, + {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, + {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +send2trash = [ + {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"}, + {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1361,13 +2389,68 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +terminado = [ + {file = "terminado-0.11.0-py3-none-any.whl", hash = "sha256:221eef83e6a504894842f7dccfa971ca2e98ec22a8a9118577e5257527674b42"}, + {file = "terminado-0.11.0.tar.gz", hash = "sha256:1e01183885f64c1bba3cf89a5a995ad4acfed4e5f00aebcce1bf7f089b0825a1"}, +] +testpath = [ + {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, + {file = "testpath-0.5.0.tar.gz", hash = "sha256:1acf7a0bcd3004ae8357409fc33751e16d37ccc650921da1094a86581ad1e417"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.0-py3-none-any.whl", hash = "sha256:056f0376bf5a6b182c513f9582c1e5b0487265eb6c48842b69aa9ca1cd5f640a"}, - {file = "tomli-1.2.0.tar.gz", hash = "sha256:d60e681734099207a6add7a10326bc2ddd1fdc36c1b0f547d00ef73ac63739c2"}, + {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, + {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, +] +tornado = [ + {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, + {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, + {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, + {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, + {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, + {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, + {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, + {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, + {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, + {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, + {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, + {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, + {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, + {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, + {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, + {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, + {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, + {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, +] +traitlets = [ + {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, + {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, ] typed-ast = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, @@ -1410,6 +2493,18 @@ urllib3 = [ {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] +widgetsnbextension = [ + {file = "widgetsnbextension-3.5.1-py2.py3-none-any.whl", hash = "sha256:bd314f8ceb488571a5ffea6cc5b9fc6cba0adaf88a9d2386b93a489751938bcd"}, + {file = "widgetsnbextension-3.5.1.tar.gz", hash = "sha256:079f87d87270bce047512400efd70238820751a11d2d8cb137a5a5bdbaf255c7"}, +] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] diff --git a/pyproject.toml b/pyproject.toml index 747e9a5c5..d5d7294ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,8 @@ pytest-cov = "^2.12.1" diff-cover = "^6.2.0" mypy = "^0.910" pydocstyle = "^6.1.1" +jupyter = "^1.0.0" +nbmake = "^0.5" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/script/nbmake_utils/notebook_sanitize.py b/script/nbmake_utils/notebook_sanitize.py new file mode 100644 index 000000000..d1579f2a4 --- /dev/null +++ b/script/nbmake_utils/notebook_sanitize.py @@ -0,0 +1,21 @@ +import json +import sys + +from pathlib import Path + + +def main(): + path_to_glob = Path(sys.argv[1]) + notebooks = path_to_glob.glob("*.ipynb") + + for notebook_file in notebooks: + with open(notebook_file, "r") as f: + notebook_dict = json.load(f) + notebook_dict["metadata"] = {} + + with open(notebook_file, "w", newline="\n") as f: + json.dump(notebook_dict, f, indent=1, ensure_ascii=False) + + +if __name__ == "__main__": + main() diff --git a/script/nbmake_utils/notebook_test_timeout.py b/script/nbmake_utils/notebook_test_timeout.py new file mode 100644 index 000000000..862093404 --- /dev/null +++ b/script/nbmake_utils/notebook_test_timeout.py @@ -0,0 +1,23 @@ +import json +import sys + +from pathlib import Path + + +def main(): + path_to_glob = Path(sys.argv[1]) + notebooks = path_to_glob.glob("*.ipynb") + + for notebook_file in notebooks: + with open(notebook_file, "r") as f: + notebook_dict = json.load(f) + execution = notebook_dict["metadata"].get("execution", {}) + execution["timeout"] = 1000 + notebook_dict["metadata"]["execution"] = execution + + with open(notebook_file, "w", newline="\n") as f: + json.dump(notebook_dict, f, indent=1, ensure_ascii=False) + + +if __name__ == "__main__": + main() diff --git a/torch_requirements.txt b/torch_requirements.txt new file mode 100644 index 000000000..e3d601f0c --- /dev/null +++ b/torch_requirements.txt @@ -0,0 +1,7 @@ +torch==1.9.0; sys_platform=="darwin" +torch==1.9.0+cpu; sys_platform=="linux" +torch==1.9.0+cpu; sys_platform=="win32" + +torchvision==0.10.0; sys_platform=="darwin" +torchvision==0.10.0+cpu; sys_platform=="linux" +torchvision==0.10.0+cpu; sys_platform=="win32" From 8fbe5dab4d7bec2455f1418652f517a8b1ad80d8 Mon Sep 17 00:00:00 2001 From: youben11 Date: Mon, 16 Aug 2021 09:36:48 +0100 Subject: [PATCH 0087/1104] fix(mlir): unsigned are considered signless in compiler + changed the name of compiled func to main, as it's the default name to be executed later --- hdk/common/mlir/mlir_converter.py | 5 +++-- tests/common/mlir/test_mlir_converter.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hdk/common/mlir/mlir_converter.py b/hdk/common/mlir/mlir_converter.py index 2e443b785..d1f0d8668 100644 --- a/hdk/common/mlir/mlir_converter.py +++ b/hdk/common/mlir/mlir_converter.py @@ -58,7 +58,8 @@ class MLIRConverter: dtype = cast(Integer, value.data_type) if dtype.is_signed: return IntegerType.get_signed(dtype.bit_width, context=self.context) - return IntegerType.get_unsigned(dtype.bit_width, context=self.context) + # unsigned integer are considered signless in the compiler + return IntegerType.get_signless(dtype.bit_width, context=self.context) raise TypeError(f"can't convert value of type {type(value)} to MLIR type") def convert(self, op_graph: OPGraph) -> str: @@ -80,7 +81,7 @@ class MLIRConverter: ] @builtin.FuncOp.from_py_func(*func_types) - def fhe_circuit(*arg): + def main(*arg): ir_to_mlir_node = {} for arg_num, node in op_graph.input_nodes.items(): ir_to_mlir_node[node] = arg[arg_num] diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 851405072..1537d0cf2 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -157,7 +157,7 @@ def test_hdk_clear_integer_to_mlir_type(is_signed): if is_signed: assert int_mlir == IntegerType.get_signed(5) else: - assert int_mlir == IntegerType.get_unsigned(5) + assert int_mlir == IntegerType.get_signless(5) def test_failing_hdk_to_mlir_type(): From 3922bfe9b4e6587eeb2303748e367354e9c3ca0e Mon Sep 17 00:00:00 2001 From: youben11 Date: Mon, 16 Aug 2021 10:41:48 +0100 Subject: [PATCH 0088/1104] feat(mlir): support constant inputs in mlir conversion --- hdk/common/mlir/converters.py | 24 +++++++++++++++- tests/common/mlir/test_converters.py | 29 ++++++++++++++++--- tests/common/mlir/test_mlir_converter.py | 36 ++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py index f9a680ac0..09fa646d3 100644 --- a/hdk/common/mlir/converters.py +++ b/hdk/common/mlir/converters.py @@ -7,8 +7,14 @@ Converter functions all have the same signature `converter(node, preds, ir_to_ml - `ctx`: MLIR context """ # pylint: disable=no-name-in-module,no-member +from typing import cast + +from mlir.dialects import std as std_dialect +from mlir.ir import IntegerAttr, IntegerType from zamalang.dialects import hlfhe +from hdk.common.data_types.integers import Integer + from ..data_types.dtypes_helpers import ( value_is_clear_integer, value_is_encrypted_unsigned_integer, @@ -113,6 +119,22 @@ def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): ).result -V0_OPSET_CONVERSION_FUNCTIONS = {ir.Add: add, ir.Sub: sub, ir.Mul: mul} +def constant(node, _, __, ctx): + """Converter function for constant inputs.""" + if not value_is_clear_integer(node.outputs[0]): + raise TypeError("Don't support non-integer constants") + dtype = cast(Integer, node.outputs[0].data_type) + if dtype.is_signed: + raise TypeError("Don't support signed constant integer") + int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) + return std_dialect.ConstantOp(int_type, IntegerAttr.get(int_type, node.constant_data)).result + + +V0_OPSET_CONVERSION_FUNCTIONS = { + ir.Add: add, + ir.Sub: sub, + ir.Mul: mul, + ir.ConstantInput: constant, +} # pylint: enable=no-name-in-module,no-member diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index 26d100925..894d42a77 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -1,15 +1,24 @@ """Test converter functions""" import pytest -from hdk.common.mlir.converters import add, mul, sub +from hdk.common.data_types.floats import Float +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearValue +from hdk.common.mlir.converters import add, constant, mul, sub class MockNode: """Mocking an intermediate node""" - def __init__(self, inputs=5, outputs=5): - self.inputs = [None for i in range(inputs)] - self.outputs = [None for i in range(outputs)] + def __init__(self, inputs_n=5, outputs_n=5, inputs=None, outputs=None): + if inputs is None: + self.inputs = [None for i in range(inputs_n)] + else: + self.inputs = inputs + if outputs is None: + self.outputs = [None for i in range(outputs_n)] + else: + self.outputs = outputs @pytest.mark.parametrize("converter", [add, sub, mul]) @@ -17,3 +26,15 @@ def test_failing_converter(converter): """Test failing converter""" with pytest.raises(TypeError, match=r"Don't support .* between .* and .*"): converter(MockNode(2, 1), None, None, None) + + +def test_fail_non_integer_const(): + """Test failing constant converter with non-integer""" + with pytest.raises(TypeError, match=r"Don't support non-integer constants"): + constant(MockNode(outputs=[ClearValue(Float(32))]), None, None, None) + + +def test_fail_signed_integer_const(): + """Test failing constant converter with non-integer""" + with pytest.raises(TypeError, match=r"Don't support signed constant integer"): + constant(MockNode(outputs=[ClearValue(Integer(8, True))]), None, None, None) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 1537d0cf2..22528a808 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -18,16 +18,31 @@ def add(x, y): return x + y +def constant_add(x): + """Test constant add""" + return x + 5 + + def sub(x, y): """Test simple sub""" return x - y +def constant_sub(x): + """Test constant sub""" + return 8 - x + + def mul(x, y): """Test simple mul""" return x * y +def constant_mul(x): + """Test constant mul""" + return x * 2 + + def sub_add_mul(x, y, z): """Test combination of ops""" return z - y + x * z @@ -60,6 +75,13 @@ def datagen(*args): }, (range(0, 8), range(1, 4)), ), + ( + constant_add, + { + "x": EncryptedValue(Integer(64, is_signed=False)), + }, + (range(0, 8),), + ), ( add, { @@ -84,6 +106,13 @@ def datagen(*args): }, (range(5, 10), range(2, 6)), ), + ( + constant_sub, + { + "x": EncryptedValue(Integer(64, is_signed=False)), + }, + (range(0, 5),), + ), ( mul, { @@ -92,6 +121,13 @@ def datagen(*args): }, (range(1, 5), range(2, 8)), ), + ( + constant_mul, + { + "x": EncryptedValue(Integer(64, is_signed=False)), + }, + (range(0, 8),), + ), ( mul, { From 8dfed58829515d3f8328e56cb7f036e7f141c676 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 18:28:05 +0200 Subject: [PATCH 0089/1104] chore(tools): add flake8 linter, flake8-bugbear plugin and fix issues - remove detected `from hdk imports` (use relative imports instead) we ARE the package - change the way Float32 and Float64 are defined --- Makefile | 9 ++- hdk/common/data_types/floats.py | 6 +- hdk/common/debugging/draw_graph.py | 4 +- hdk/common/mlir/converters.py | 3 +- hdk/hnumpy/compile.py | 4 +- poetry.lock | 88 +++++++++++++++++++++--------- pyproject.toml | 2 + tests/hnumpy/test_tracing.py | 7 ++- 8 files changed, 84 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index ae6779345..57595d0dd 100644 --- a/Makefile +++ b/Makefile @@ -24,10 +24,17 @@ pylint: poetry run pylint --rcfile=pylintrc hdk tests .PHONY: pylint +flake8: + poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" hdk/ tests/ +.PHONY: flake8 + +python_linting: pylint flake8 +.PHONY: python_linting + conformance: python_format .PHONY: conformance -pcc: check_python_format pylint mypy_ci pydocstyle +pcc: check_python_format python_linting mypy_ci pydocstyle .PHONY: pcc pytest: diff --git a/hdk/common/data_types/floats.py b/hdk/common/data_types/floats.py index d9dcd7128..9161bb391 100644 --- a/hdk/common/data_types/floats.py +++ b/hdk/common/data_types/floats.py @@ -1,5 +1,7 @@ """This file holds the definitions for floating point types.""" +from functools import partial + from . import base @@ -21,5 +23,5 @@ class Float(base.BaseDataType): return isinstance(other, self.__class__) and self.bit_width == other.bit_width -Float32 = lambda: Float(32) -Float64 = lambda: Float(64) +Float32 = partial(Float, 32) +Float64 = partial(Float, 64) diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index abb8a13aa..b46daf631 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -4,8 +4,8 @@ from typing import Any, Dict, List import matplotlib.pyplot as plt import networkx as nx -from hdk.common.operator_graph import OPGraph -from hdk.common.representation import intermediate as ir +from ..operator_graph import OPGraph +from ..representation import intermediate as ir IR_NODE_COLOR_MAPPING = { ir.Input: "blue", diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py index 09fa646d3..b57bffea6 100644 --- a/hdk/common/mlir/converters.py +++ b/hdk/common/mlir/converters.py @@ -13,8 +13,7 @@ from mlir.dialects import std as std_dialect from mlir.ir import IntegerAttr, IntegerType from zamalang.dialects import hlfhe -from hdk.common.data_types.integers import Integer - +from ...common.data_types.integers import Integer from ..data_types.dtypes_helpers import ( value_is_clear_integer, value_is_encrypted_unsigned_integer, diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 90b417e0e..89eb79713 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -2,9 +2,7 @@ from typing import Any, Callable, Dict, Iterator, Optional, Tuple -from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset -from hdk.hnumpy.tracing import trace_numpy_function - +from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from ..common.compilation import CompilationArtifacts from ..common.data_types import BaseValue from ..common.mlir.utils import ( diff --git a/poetry.lock b/poetry.lock index 74a018f89..b23d699cb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -269,6 +269,35 @@ category = "dev" optional = false python-versions = ">=2.7" +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "flake8-bugbear" +version = "21.4.3" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "black", "hypothesis", "hypothesmith"] + [[package]] name = "idna" version = "3.2" @@ -979,6 +1008,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pycparser" version = "2.20" @@ -1016,6 +1053,14 @@ snowballstemmer = "*" [package.extras] toml = ["toml"] +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pygments" version = "2.9.0" @@ -1494,7 +1539,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "1c23e8e0262499b55c7652407e635835efe43581d63693d668c6f2ed812d5f56" +content-hash = "c3feb324b9cc9a0e9779ae43ec2e26a1421ecd29e75880b52cacecd03c56cf17" [metadata.files] alabaster = [ @@ -1710,6 +1755,14 @@ entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-21.4.3.tar.gz", hash = "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7"}, + {file = "flake8_bugbear-21.4.3-py36.py37.py38-none-any.whl", hash = "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa"}, +] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, @@ -1854,22 +1907,12 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1878,21 +1921,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1902,9 +1938,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2066,11 +2099,6 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pillow = [ - {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, - {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, - {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, - {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, - {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, @@ -2126,6 +2154,10 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] pycparser = [ {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, @@ -2158,6 +2190,10 @@ pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] pygments = [ {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, diff --git a/pyproject.toml b/pyproject.toml index d5d7294ac..0c7ee8b2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ mypy = "^0.910" pydocstyle = "^6.1.1" jupyter = "^1.0.0" nbmake = "^0.5" +flake8 = "^3.9.2" +flake8-bugbear = "^21.4.3" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 1649aa2a5..e361d2dd1 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -10,10 +10,12 @@ from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.representation import intermediate as ir from hdk.hnumpy import tracing +OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] + @pytest.mark.parametrize( "operation", - [ir.Add, ir.Sub, ir.Mul], + OPERATIONS_TO_TEST, ) @pytest.mark.parametrize( "x", @@ -69,14 +71,13 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): z = x + x return z * y + assert operation in OPERATIONS_TO_TEST, f"unknown operation {operation}" if operation == ir.Add: function_to_compile = simple_add_function elif operation == ir.Sub: function_to_compile = simple_sub_function elif operation == ir.Mul: function_to_compile = simple_mul_function - else: - assert False, f"unknown operation {operation}" op_graph = tracing.trace_numpy_function(function_to_compile, {"x": x, "y": y}) From 8df212ff4976af441d158113cf4d80d42fcb4dd8 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 17 Aug 2021 13:08:42 +0300 Subject: [PATCH 0090/1104] bench: create the benchmarking infrastructure --- .gitignore | 3 + Makefile | 18 +- benchmarks/test_compilation_and_evaluation.py | 80 ++++++++ poetry.lock | 182 +++++++++++------- pyproject.toml | 1 + 5 files changed, 211 insertions(+), 73 deletions(-) create mode 100644 benchmarks/test_compilation_and_evaluation.py diff --git a/.gitignore b/.gitignore index d099f2934..242a2ba9b 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,6 @@ dmypy.json # Pyre type checker .pyre/ + +# pytest-benchmark results +.benchmarks diff --git a/Makefile b/Makefile index 57595d0dd..762cfd062 100644 --- a/Makefile +++ b/Makefile @@ -13,19 +13,19 @@ sync_env: .PHONY: sync_env python_format: - poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests + poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --dir benchmarks .PHONY: python_format check_python_format: - poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --check + poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --dir benchmarks --check .PHONY: check_python_format pylint: - poetry run pylint --rcfile=pylintrc hdk tests + poetry run pylint --rcfile=pylintrc hdk tests benchmarks .PHONY: pylint flake8: - poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" hdk/ tests/ + poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" hdk/ tests/ benchmarks/ .PHONY: flake8 python_linting: pylint flake8 @@ -56,7 +56,11 @@ mypy_test: find ./tests/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports .PHONY: mypy_test -mypy_ci: mypy mypy_test +mypy_benchmark: + find ./benchmarks/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports +.PHONY: mypy_benchmark + +mypy_ci: mypy mypy_test mypy_benchmark .PHONY: mypy_ci pytest_and_coverage: pytest coverage @@ -117,3 +121,7 @@ strip_nb: notebook_timeout: poetry run python ./script/nbmake_utils/notebook_test_timeout.py examples .PHONY: notebook_timeout + +benchmark: + poetry run pytest benchmarks/ --benchmark-save=findings +.PHONY: benchmark diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py new file mode 100644 index 000000000..c44e5a396 --- /dev/null +++ b/benchmarks/test_compilation_and_evaluation.py @@ -0,0 +1,80 @@ +"""Benchmark module for the entire compilation pipeline""" + +import itertools + +import pytest + +from hdk.common.data_types.integers import SignedInteger, UnsignedInteger +from hdk.common.data_types.values import EncryptedValue +from hdk.hnumpy.compile import compile_numpy_function + + +@pytest.mark.parametrize( + "function,parameters,ranges", + [ + pytest.param( + lambda x: x + 42, + {"x": EncryptedValue(SignedInteger(4))}, + ((-2, 2),), + id="x + 42", + ), + pytest.param( + lambda x, y: x + y, + {"x": EncryptedValue(SignedInteger(4)), "y": EncryptedValue(UnsignedInteger(4))}, + ((-2, 2), (20, 30)), + id="x + y", + ), + ], +) +def test_compilation(benchmark, function, parameters, ranges): + """Benchmark function for compilation of various functions""" + + def dataset(args): + for prod in itertools.product(*args): + yield prod + + @benchmark + def compilation(): + compile_numpy_function(function, parameters, dataset(ranges)) + + +@pytest.mark.parametrize( + "function,parameters,ranges,inputs", + [ + pytest.param( + lambda x: x + 420, + {"x": EncryptedValue(SignedInteger(4))}, + ((-2, 2),), + [ + {0: -2}, + {0: 0}, + {0: 1}, + ], + id="x + 420", + ), + pytest.param( + lambda x, y: x + y, + {"x": EncryptedValue(SignedInteger(4)), "y": EncryptedValue(UnsignedInteger(4))}, + ((-2, 2), (20, 30)), + [ + {0: -2, 1: 25}, + {0: 0, 1: 30}, + {0: 1, 1: 22}, + ], + id="x + y", + ), + ], +) +def test_evaluation(benchmark, function, parameters, ranges, inputs): + """Benchmark function for evaluation of various functions""" + + def dataset(args): + for prod in itertools.product(*args): + yield prod + + graph = compile_numpy_function(function, parameters, dataset(ranges)) + + @benchmark + def evaluation(): + for x in inputs: + graph.evaluate(x) diff --git a/poetry.lock b/poetry.lock index b23d699cb..e9ad497d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -53,19 +53,11 @@ typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpyth typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" -[[package]] -name = "async-generator" -version = "1.10" -description = "Async generators and context managers for Python 3.5+" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -237,7 +229,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.3.0" +version = "6.3.1" description = "Run coverage and linting reports on diffs" category = "dev" optional = false @@ -316,9 +308,9 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.3" +version = "4.6.4" description = "Read metadata from Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -347,7 +339,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -739,14 +731,13 @@ testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest [[package]] name = "nbclient" -version = "0.5.3" +version = "0.5.4" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." category = "dev" optional = false python-versions = ">=3.6.1" [package.dependencies] -async-generator = "*" jupyter-client = ">=6.1.5" nbformat = ">=5.0" nest-asyncio = "*" @@ -876,11 +867,11 @@ test = ["pytest", "coverage", "requests", "nbval", "selenium", "pytest-cov", "re [[package]] name = "numpy" -version = "1.21.1" +version = "1.21.2" description = "NumPy is the fundamental package for array computing with Python." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.7,<3.11" [[package]] name = "packaging" @@ -960,7 +951,7 @@ python-versions = ">=3.6" name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1004,10 +995,18 @@ python-versions = "*" name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "py-cpuinfo" +version = "8.0.0" +description = "Get CPU info with pure Python 2 & 3" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pycodestyle" version = "2.7.0" @@ -1063,7 +1062,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.9.0" +version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -1104,7 +1103,7 @@ python-versions = ">=3.6" name = "pytest" version = "6.2.4" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1122,6 +1121,23 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-benchmark" +version = "3.4.1" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=3.8" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs"] + [[package]] name = "pytest-cov" version = "2.12.1" @@ -1217,11 +1233,11 @@ test = ["flaky", "pytest", "pytest-qt"] [[package]] name = "qtpy" -version = "1.9.0" +version = "1.10.0" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" [[package]] name = "regex" @@ -1426,7 +1442,7 @@ test = ["pytest", "pathlib2"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -1528,7 +1544,7 @@ python-versions = "*" name = "zipp" version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1539,7 +1555,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "c3feb324b9cc9a0e9779ae43ec2e26a1421ecd29e75880b52cacecd03c56cf17" +content-hash = "e4c9439ab07d0be04d57d753d399bcca05f00331cd45eee889f732246d4edef4" [metadata.files] alabaster = [ @@ -1582,10 +1598,6 @@ astroid = [ {file = "astroid-2.6.6-py3-none-any.whl", hash = "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef"}, {file = "astroid-2.6.6.tar.gz", hash = "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334"}, ] -async-generator = [ - {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, - {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, -] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, @@ -1744,8 +1756,8 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.3.0-py3-none-any.whl", hash = "sha256:105b82e897a0a594ef5928a745a23edb06ff213bcb4d31644967fad9576e1b96"}, - {file = "diff_cover-6.3.0.tar.gz", hash = "sha256:f55255a8855c6d2f58b1f9cd967bf27cd5c84fcc1630b785d405625aa17be99f"}, + {file = "diff_cover-6.3.1-py3-none-any.whl", hash = "sha256:2578fb51c4a5ce162d9ba7f5dcc28132b55539c889e9b648f9df77d4fdcf8fb4"}, + {file = "diff_cover-6.3.1.tar.gz", hash = "sha256:21baf9d6f40ef352df4adf19b5bb4d47249c540a648fecda4647a41ff558d47c"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1772,8 +1784,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"}, - {file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"}, + {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"}, + {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"}, ] inflect = [ {file = "inflect-5.3.0-py3-none-any.whl", hash = "sha256:42560be16af702a21d43d59427f276b5aed79efb1ded9b713468c081f4353d10"}, @@ -1907,12 +1919,22 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1921,14 +1943,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1938,6 +1967,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2015,8 +2047,8 @@ myst-parser = [ {file = "myst_parser-0.15.1-py3-none-any.whl", hash = "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"}, ] nbclient = [ - {file = "nbclient-0.5.3-py3-none-any.whl", hash = "sha256:e79437364a2376892b3f46bedbf9b444e5396cfb1bc366a472c37b48e9551500"}, - {file = "nbclient-0.5.3.tar.gz", hash = "sha256:db17271330c68c8c88d46d72349e24c147bb6f34ec82d8481a8f025c4d26589c"}, + {file = "nbclient-0.5.4-py3-none-any.whl", hash = "sha256:95a300c6fbe73721736cf13972a46d8d666f78794b832866ed7197a504269e11"}, + {file = "nbclient-0.5.4.tar.gz", hash = "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce"}, ] nbconvert = [ {file = "nbconvert-6.1.0-py3-none-any.whl", hash = "sha256:37cd92ff2ae6a268e62075ff8b16129e0be4939c4dfcee53dc77cc8a7e06c684"}, @@ -2043,34 +2075,36 @@ notebook = [ {file = "notebook-6.4.3.tar.gz", hash = "sha256:e6b6dfed36b00cf950f63c0d42e947c101d4258aec21624de62b9e0c11ed5c0d"}, ] numpy = [ - {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, - {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, - {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, - {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, - {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, - {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, - {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, - {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, - {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, - {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, - {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, - {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, - {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, - {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, - {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, - {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, - {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, - {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, - {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, - {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, + {file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13"}, + {file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a7b9db0a2941434cd930dacaafe0fc9da8f3d6157f9d12f761bbde93f46218"}, + {file = "numpy-1.21.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f2dc79c093f6c5113718d3d90c283f11463d77daa4e83aeeac088ec6a0bda52"}, + {file = "numpy-1.21.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a55e4d81c4260386f71d22294795c87609164e22b28ba0d435850fbdf82fc0c5"}, + {file = "numpy-1.21.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:426a00b68b0d21f2deb2ace3c6d677e611ad5a612d2c76494e24a562a930c254"}, + {file = "numpy-1.21.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:298156f4d3d46815eaf0fcf0a03f9625fc7631692bd1ad851517ab93c3168fc6"}, + {file = "numpy-1.21.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09858463db6dd9f78b2a1a05c93f3b33d4f65975771e90d2cf7aadb7c2f66edf"}, + {file = "numpy-1.21.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:805459ad8baaf815883d0d6f86e45b3b0b67d823a8f3fa39b1ed9c45eaf5edf1"}, + {file = "numpy-1.21.2-cp37-cp37m-win32.whl", hash = "sha256:f545c082eeb09ae678dd451a1b1dbf17babd8a0d7adea02897a76e639afca310"}, + {file = "numpy-1.21.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b160b9a99ecc6559d9e6d461b95c8eec21461b332f80267ad2c10394b9503496"}, + {file = "numpy-1.21.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a5109345f5ce7ddb3840f5970de71c34a0ff7fceb133c9441283bb8250f532a3"}, + {file = "numpy-1.21.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:209666ce9d4a817e8a4597cd475b71b4878a85fa4b8db41d79fdb4fdee01dde2"}, + {file = "numpy-1.21.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c01b59b33c7c3ba90744f2c695be571a3bd40ab2ba7f3d169ffa6db3cfba614f"}, + {file = "numpy-1.21.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e42029e184008a5fd3d819323345e25e2337b0ac7f5c135b7623308530209d57"}, + {file = "numpy-1.21.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7fdc7689daf3b845934d67cb221ba8d250fdca20ac0334fea32f7091b93f00d3"}, + {file = "numpy-1.21.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550564024dc5ceee9421a86fc0fb378aa9d222d4d0f858f6669eff7410c89bef"}, + {file = "numpy-1.21.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf75d5825ef47aa51d669b03ce635ecb84d69311e05eccea083f31c7570c9931"}, + {file = "numpy-1.21.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a9da45b748caad72ea4a4ed57e9cd382089f33c5ec330a804eb420a496fa760f"}, + {file = "numpy-1.21.2-cp38-cp38-win32.whl", hash = "sha256:e167b9805de54367dcb2043519382be541117503ce99e3291cc9b41ca0a83557"}, + {file = "numpy-1.21.2-cp38-cp38-win_amd64.whl", hash = "sha256:466e682264b14982012887e90346d33435c984b7fead7b85e634903795c8fdb0"}, + {file = "numpy-1.21.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dd0e3651d210068d13e18503d75aaa45656eef51ef0b261f891788589db2cc38"}, + {file = "numpy-1.21.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92a0ab128b07799dd5b9077a9af075a63467d03ebac6f8a93e6440abfea4120d"}, + {file = "numpy-1.21.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fde50062d67d805bc96f1a9ecc0d37bfc2a8f02b937d2c50824d186aa91f2419"}, + {file = "numpy-1.21.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640c1ccfd56724f2955c237b6ccce2e5b8607c3bc1cc51d3933b8c48d1da3723"}, + {file = "numpy-1.21.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5de64950137f3a50b76ce93556db392e8f1f954c2d8207f78a92d1f79aa9f737"}, + {file = "numpy-1.21.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b342064e647d099ca765f19672696ad50c953cac95b566af1492fd142283580f"}, + {file = "numpy-1.21.2-cp39-cp39-win32.whl", hash = "sha256:30fc68307c0155d2a75ad19844224be0f2c6f06572d958db4e2053f816b859ad"}, + {file = "numpy-1.21.2-cp39-cp39-win_amd64.whl", hash = "sha256:b5e8590b9245803c849e09bae070a8e1ff444f45e3f0bed558dd722119eea724"}, + {file = "numpy-1.21.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d96a6a7d74af56feb11e9a443150216578ea07b7450f7c05df40eec90af7f4a7"}, + {file = "numpy-1.21.2.zip", hash = "sha256:423216d8afc5923b15df86037c6053bf030d15cc9e3224206ef868c2d63dd6dc"}, ] packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, @@ -2099,6 +2133,11 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pillow = [ + {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, + {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, + {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, + {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, + {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, @@ -2154,6 +2193,9 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +py-cpuinfo = [ + {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -2195,8 +2237,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, - {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pylint = [ {file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"}, @@ -2233,6 +2275,10 @@ pytest = [ {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] +pytest-benchmark = [ + {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, + {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, +] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, @@ -2339,8 +2385,8 @@ qtconsole = [ {file = "qtconsole-5.1.1.tar.gz", hash = "sha256:bbc34bca14f65535afcb401bc74b752bac955e5313001ba640383f7e5857dc49"}, ] qtpy = [ - {file = "QtPy-1.9.0-py2.py3-none-any.whl", hash = "sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea"}, - {file = "QtPy-1.9.0.tar.gz", hash = "sha256:2db72c44b55d0fe1407be8fba35c838ad0d6d3bb81f23007886dc1fc0f459c8d"}, + {file = "QtPy-1.10.0-py2.py3-none-any.whl", hash = "sha256:f683ce6cd825ba8248a798bf1dfa1a07aca387c88ae44fa5479537490aace7be"}, + {file = "QtPy-1.10.0.tar.gz", hash = "sha256:3d20f010caa3b2c04835d6a2f66f8873b041bdaf7a76085c2a0d7890cdd65ea9"}, ] regex = [ {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, diff --git a/pyproject.toml b/pyproject.toml index 0c7ee8b2a..37d500f7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ myst-parser = "^0.15.1" networkx = "^2.6.1" matplotlib = "^3.4.2" numpy = "^1.21.1" +pytest-benchmark = "^3.4.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" From 553f77f19bc6fcf618aefa1cd6f80d96c0ce9be9 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 17 Aug 2021 18:03:57 +0300 Subject: [PATCH 0091/1104] chore(ci): implement publishing docs to hdk.zama.ai on push to main --- .github/workflows/continuous-integration.yaml | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index c498d1933..aa4a51884 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -1,6 +1,9 @@ -name: hdk PR checks - -on: [pull_request] +name: HDK CI Pipeline +on: + pull_request: + push: + branches: + - main jobs: build: @@ -104,3 +107,50 @@ jobs: SLACK_MESSAGE: 'Build finished with status ${{ job.status }}' SLACK_USERNAME: zama-bot SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + publish-docs: + needs: [build] + + runs-on: ubuntu-20.04 + if: ${{ github.event_name == 'push' && github.base_ref == 'main' }} + + steps: + - name: Download Documentation + id: download + uses: actions/download-artifact@v2 + with: + name: html-docs + + - name: Publish Documentation to S3 + id: publish + if: ${{ steps.download.outcome == 'success' && !cancelled() }} + uses: jakejarvis/s3-sync-action@master + with: + args: --delete + env: + AWS_S3_BUCKET: ${{ secrets.AWS_HDK_DOCUMENTATION_BUCKET_NAME }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SOURCE_DIR: '.' + + - name: Invalidate CloudFront Cache + if: ${{ steps.publish.outcome == 'success' }} + uses: awact/cloudfront-action@master + env: + SOURCE_PATH: '/*' + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DISTRIBUTION_ID: ${{ secrets.AWS_HDK_DOCUMENTATION_DISTRIBUTION_ID }} + + - name: Slack Notification + if: ${{ always() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: hdk-updates + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: 'Publishing documentation finished with status ${{ job.status }}' + SLACK_USERNAME: zama-bot + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From 63eac35a43fdcdc46afdb76dafaded065b94eba6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 12 Aug 2021 12:10:17 +0200 Subject: [PATCH 0092/1104] build: launch pcc targets in parallel to speed up checks - change call in github actions as proper flags are in the Makefile - change mypy_ci target to avoid cache issues with multiple mypy instances --- .github/workflows/continuous-integration.yaml | 5 ++--- Makefile | 11 +++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index aa4a51884..88081520c 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -54,10 +54,9 @@ jobs: - name: Conformance id: conformance if: ${{ success() && !cancelled() }} - # keep going registers errors in the intermediate targets but executes them all - # Nicer to have pcc complete for the dev and have all the relevant conformance issues + # pcc launches an internal target with proper flags run: | - make --keep-going pcc + make pcc - name: PyTest id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} diff --git a/Makefile b/Makefile index 762cfd062..df67344ca 100644 --- a/Makefile +++ b/Makefile @@ -34,9 +34,13 @@ python_linting: pylint flake8 conformance: python_format .PHONY: conformance -pcc: check_python_format python_linting mypy_ci pydocstyle +pcc: + @$(MAKE) --keep-going --jobs $$(nproc) --output-sync --no-print-directory pcc_internal .PHONY: pcc +pcc_internal: check_python_format python_linting mypy_ci pydocstyle +.PHONY: pcc_internal + pytest: poetry run pytest --cov=hdk -vv --cov-report=xml tests/ .PHONY: pytest @@ -60,7 +64,10 @@ mypy_benchmark: find ./benchmarks/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports .PHONY: mypy_benchmark -mypy_ci: mypy mypy_test mypy_benchmark +mypy_ci: + @$(MAKE) --no-print-directory mypy + @$(MAKE) --no-print-directory mypy_test + @$(MAKE) --no-print-directory mypy_benchmark .PHONY: mypy_ci pytest_and_coverage: pytest coverage From 4976855c1d344dedbf2bcec5100c0fd906e29c6e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 12:14:16 +0200 Subject: [PATCH 0093/1104] fix(debug): manage all get_color cases and unsorted arg names --- hdk/common/debugging/draw_graph.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/draw_graph.py index b46daf631..fc63401b4 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/draw_graph.py @@ -13,6 +13,7 @@ IR_NODE_COLOR_MAPPING = { ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green", + ir.ArbitraryFunction: "orange", "ArbitraryFunction": "orange", "TLU": "grey", "output": "magenta", @@ -114,11 +115,12 @@ def draw_graph( # Colors and labels def get_color(node): + value_to_return = IR_NODE_COLOR_MAPPING[type(node)] if node in set_of_nodes_which_are_outputs: - return IR_NODE_COLOR_MAPPING["output"] - if isinstance(node, ir.ArbitraryFunction): - return IR_NODE_COLOR_MAPPING[node.op_name] - return IR_NODE_COLOR_MAPPING[type(node)] + value_to_return = IR_NODE_COLOR_MAPPING["output"] + elif isinstance(node, ir.ArbitraryFunction): + value_to_return = IR_NODE_COLOR_MAPPING.get(node.op_name, value_to_return) + return value_to_return color_map = [get_color(node) for node in graph.nodes()] @@ -273,11 +275,11 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: list_of_arg_name += [(index["input_idx"], str(map_table[pred]))] # Some checks, because the previous algorithm is not clear - assert len(list_of_arg_name) == len({x[0] for x in list_of_arg_name}) + assert len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name)) + list_of_arg_name.sort() assert [x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name))) # Then, just print the predecessors in the right order - list_of_arg_name.sort() what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" new_line = f"%{i} = {what_to_print}" From 0eebbfcd26eb960d2ff49567c6366afef422243e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 12:31:07 +0200 Subject: [PATCH 0094/1104] dev(opgraph): add facilities to OPGraph - allow to construct graph from an existing networkx MultiDiGraph - add a function to remove nodes unreachable from the outputs of the graph - return the evaluated output when calling the OPGraph --- hdk/common/operator_graph.py | 100 ++++++++++++++++++++++++++++++----- hdk/hnumpy/tracing.py | 2 +- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index 14bd2b539..eecc4ac85 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -1,7 +1,7 @@ """Code to wrap and make manipulating networkx graphs easier.""" from copy import deepcopy -from typing import Any, Dict, Iterable, List, Mapping +from typing import Any, Dict, Iterable, List, Set, Tuple, Union import networkx as nx @@ -16,20 +16,79 @@ class OPGraph: """Class to make work with nx graphs easier.""" graph: nx.MultiDiGraph - input_nodes: Mapping[int, ir.Input] - output_nodes: Mapping[int, ir.IntermediateNode] + input_nodes: Dict[int, ir.Input] + output_nodes: Dict[int, ir.IntermediateNode] - def __init__(self, output_tracers: Iterable[BaseTracer]) -> None: - self.output_nodes = { + def __init__( + self, + graph: nx.MultiDiGraph, + input_nodes: Dict[int, ir.Input], + output_nodes: Dict[int, ir.IntermediateNode], + ) -> None: + assert len(input_nodes) > 0, "Got a graph without input nodes which is not supported" + assert all( + isinstance(node, ir.Input) for node in input_nodes.values() + ), "Got input nodes that were not ir.Input, which is not supported" + assert all( + isinstance(node, ir.IntermediateNode) for node in output_nodes.values() + ), "Got output nodes which were not ir.IntermediateNode, which is not supported" + + self.graph = graph + self.input_nodes = input_nodes + self.output_nodes = output_nodes + self.prune_nodes() + + def __call__(self, *args) -> Union[Any, Tuple[Any, ...]]: + inputs = dict(enumerate(args)) + + assert len(inputs) == len( + self.input_nodes + ), f"Expected {len(self.input_nodes)} arguments, got {len(inputs)} : {args}" + + results = self.evaluate(inputs) + tuple_result = tuple(results[output_node] for output_node in self.get_ordered_outputs()) + return tuple_result if len(tuple_result) > 1 else tuple_result[0] + + @staticmethod + def from_output_tracers(output_tracers: Iterable[BaseTracer]) -> "OPGraph": + """Construct OPGraph from output tracers. + + Args: + output_tracers (Iterable[BaseTracer]): The tracers output by the function that was + traced. + + Returns: + OPGraph: The resulting OPGraph. + """ + graph = create_graph_from_output_tracers(output_tracers) + input_nodes = { + node.program_input_idx: node + for node in graph.nodes() + if len(graph.pred[node]) == 0 and isinstance(node, ir.Input) + } + output_nodes = { output_idx: tracer.traced_computation for output_idx, tracer in enumerate(output_tracers) } - self.graph = create_graph_from_output_tracers(output_tracers) - self.input_nodes = { - node.program_input_idx: node - for node in self.graph.nodes() - if len(self.graph.pred[node]) == 0 and isinstance(node, ir.Input) - } + return OPGraph(graph, input_nodes, output_nodes) + + @staticmethod + def from_graph( + graph: nx.MultiDiGraph, + input_nodes: Iterable[ir.Input], + output_nodes: Iterable[ir.IntermediateNode], + ) -> "OPGraph": + """Construct OPGraph from an existing networkx MultiDiGraph. + + Args: + graph (nx.MultiDiGraph): The networkx MultiDiGraph to use. + input_nodes (Iterable[ir.Input]): The input nodes of the MultiDiGraph. + output_nodes (Iterable[ir.IntermediateNode]): The output nodes of the MultiDiGraph. + + Returns: + OPGraph: The resulting OPGraph. + """ + return OPGraph(graph, dict(enumerate(input_nodes)), dict(enumerate(output_nodes))) def get_ordered_inputs(self) -> List[ir.Input]: """Get the input nodes of the graph, ordered by their index. @@ -47,11 +106,11 @@ class OPGraph: """ return [self.output_nodes[idx] for idx in range(len(self.output_nodes))] - def evaluate(self, inputs: Mapping[int, Any]) -> Dict[ir.IntermediateNode, Any]: + def evaluate(self, inputs: Dict[int, Any]) -> Dict[ir.IntermediateNode, Any]: """Function to evaluate a graph and get intermediate values for all nodes. Args: - inputs (Mapping[int, Any]): The inputs to the program + inputs (Dict[int, Any]): The inputs to the program Returns: Dict[ir.IntermediateNode, Any]: Dictionary with node as keys and resulting values @@ -119,3 +178,18 @@ class OPGraph: for edge in edge_data.values(): input_idx = edge["input_idx"] succ.inputs[input_idx] = deepcopy(node.outputs[0]) + + def prune_nodes(self): + """Function to remove unreachable nodes from outputs.""" + + current_nodes = set(self.output_nodes.values()) + useful_nodes: Set[ir.IntermediateNode] = set() + while current_nodes: + next_nodes: Set[ir.IntermediateNode] = set() + useful_nodes.update(current_nodes) + for node in current_nodes: + next_nodes.update(self.graph.pred[node]) + current_nodes = next_nodes + + useless_nodes = set(self.graph.nodes()) - useful_nodes + self.graph.remove_nodes_from(useless_nodes) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 245bbf641..a06125399 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -139,6 +139,6 @@ def trace_numpy_function( if isinstance(output_tracers, NPTracer): output_tracers = (output_tracers,) - op_graph = OPGraph(output_tracers) + op_graph = OPGraph.from_output_tracers(output_tracers) return op_graph From 825d6422d0015910ebb9ed0e555e06717f6d1f99 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 12:35:26 +0200 Subject: [PATCH 0095/1104] dev(NPTracer): add op_name for traced functions, deepcopy kwargs - ir.ArbitraryFunction does not deepcopy op_args and op_kwargs by default anymore to let the control to the developer instantiating it --- hdk/common/representation/intermediate.py | 4 ++-- hdk/hnumpy/tracing.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 0c98d8eb6..e257556d5 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -200,8 +200,8 @@ class ArbitraryFunction(IntermediateNode): super().__init__([input_base_value]) assert len(self.inputs) == 1 self.arbitrary_func = arbitrary_func - self.op_args = deepcopy(op_args) if op_args is not None else () - self.op_kwargs = deepcopy(op_kwargs) if op_kwargs is not None else {} + self.op_args = op_args if op_args is not None else () + self.op_kwargs = op_kwargs if op_kwargs is not None else {} # TLU/PBS has an encrypted output self.outputs = [EncryptedValue(output_dtype)] self.op_name = op_name if op_name is not None else self.__class__.__name__ diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index a06125399..1a4996061 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,4 +1,5 @@ """hnumpy tracing utilities.""" +from copy import deepcopy from typing import Callable, Dict, Mapping import numpy @@ -53,6 +54,7 @@ class NPTracer(BaseTracer): input_base_value=self.output, arbitrary_func=normalized_numpy_dtype.type, output_dtype=output_dtype, + op_name=f"astype({normalized_numpy_dtype})", ) output_tracer = self.__class__( [self], traced_computation=traced_computation, output_index=0 @@ -103,7 +105,8 @@ class NPTracer(BaseTracer): input_base_value=input_tracers[0].output, arbitrary_func=numpy.rint, output_dtype=common_output_dtypes[0], - op_kwargs=kwargs, + op_kwargs=deepcopy(kwargs), + op_name="numpy.rint", ) output_tracer = self.__class__( input_tracers, traced_computation=traced_computation, output_index=0 From d48c4dba32597eaef4e3a95411b8723e769ac77a Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 12:45:52 +0200 Subject: [PATCH 0096/1104] dev(NPTracer): add support for sin - re-organize numpy tracing tests refs #126 --- hdk/hnumpy/tracing.py | 23 +++++++++++++++++++++++ tests/hnumpy/test_tracing.py | 32 ++++++++++++++++---------------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 1a4996061..74416b2c7 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -113,8 +113,31 @@ class NPTracer(BaseTracer): ) return output_tracer + def sin(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.sin. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + assert len(input_tracers) == 1 + common_output_dtypes = self._manage_dtypes(numpy.sin, *input_tracers) + assert len(common_output_dtypes) == 1 + + traced_computation = ir.ArbitraryFunction( + input_base_value=input_tracers[0].output, + arbitrary_func=numpy.sin, + output_dtype=common_output_dtypes[0], + op_kwargs=deepcopy(kwargs), + op_name="numpy.sin", + ) + output_tracer = self.__class__( + input_tracers, traced_computation=traced_computation, output_index=0 + ) + return output_tracer + UFUNC_ROUTING: Mapping[numpy.ufunc, Callable] = { numpy.rint: rint, + numpy.sin: sin, } diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index e361d2dd1..3ede097c9 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -205,52 +205,51 @@ def test_tracing_astype( @pytest.mark.parametrize( - "function_to_trace,inputs,expected_output_node,expected_output_value", + "function_to_trace", [ # We cannot call trace_numpy_function on some numpy function as getting the signature for # these functions fails, so we wrap it in a lambda # pylint: disable=unnecessary-lambda + pytest.param(lambda x: numpy.rint(x)), + pytest.param(lambda x: numpy.sin(x)), + # The next test case is only for coverage purposes, to trigger the unsupported method + # exception handling + pytest.param( + lambda x: numpy.add.reduce(x), + marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), + ), + # pylint: enable=unnecessary-lambda + ], +) +@pytest.mark.parametrize( + "inputs,expected_output_node,expected_output_value", + [ pytest.param( - lambda x: numpy.rint(x), {"x": EncryptedValue(Integer(7, is_signed=False))}, ir.ArbitraryFunction, EncryptedValue(Float(64)), ), pytest.param( - lambda x: numpy.rint(x), {"x": EncryptedValue(Integer(32, is_signed=True))}, ir.ArbitraryFunction, EncryptedValue(Float(64)), ), pytest.param( - lambda x: numpy.rint(x), {"x": EncryptedValue(Integer(64, is_signed=True))}, ir.ArbitraryFunction, EncryptedValue(Float(64)), ), pytest.param( - lambda x: numpy.rint(x), {"x": EncryptedValue(Integer(128, is_signed=True))}, ir.ArbitraryFunction, None, marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), ), pytest.param( - lambda x: numpy.rint(x), {"x": EncryptedValue(Float(64))}, ir.ArbitraryFunction, EncryptedValue(Float(64)), ), - # The next test case is only for coverage purposes, to trigger the unsupported method - # exception handling - pytest.param( - lambda x: numpy.add.reduce(x), - {"x": EncryptedValue(Integer(32, is_signed=True))}, - None, - None, - marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), - ), - # pylint: enable=unnecessary-lambda ], ) def test_trace_hnumpy_supported_ufuncs( @@ -269,6 +268,7 @@ def test_trace_hnumpy_supported_ufuncs( "np_ufunc,expected_tracing_func", [ pytest.param(numpy.rint, tracing.NPTracer.rint), + pytest.param(numpy.sin, tracing.NPTracer.sin), # There is a need to test the case where the function fails, I chose numpy.conjugate which # works on complex types, as we don't talk about complex types for now this looks like a # good long term candidate to check for an unsupported function From 4e40982f5a74d942e52a85ad79ff4bbdf3647fea Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 14:04:49 +0200 Subject: [PATCH 0097/1104] feat(float-fusing): fuse float parts of an OPGraph during compilation - this allows to be compatible with the current compiler and squash float domains into a single int to int ArbitraryFunction --- hdk/common/optimization/__init__.py | 1 + hdk/common/optimization/topological.py | 240 ++++++++++++++++++ hdk/hnumpy/compile.py | 19 +- .../common/optimization/test_float_fusing.py | 107 ++++++++ tests/hnumpy/test_compile.py | 15 ++ 5 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 hdk/common/optimization/__init__.py create mode 100644 hdk/common/optimization/topological.py create mode 100644 tests/common/optimization/test_float_fusing.py diff --git a/hdk/common/optimization/__init__.py b/hdk/common/optimization/__init__.py new file mode 100644 index 000000000..f4180d6bb --- /dev/null +++ b/hdk/common/optimization/__init__.py @@ -0,0 +1 @@ +"""Module holding various optimization/simplification code.""" diff --git a/hdk/common/optimization/topological.py b/hdk/common/optimization/topological.py new file mode 100644 index 000000000..3cc249bc9 --- /dev/null +++ b/hdk/common/optimization/topological.py @@ -0,0 +1,240 @@ +"""File holding topological optimization/simplification code.""" +from copy import deepcopy +from typing import Dict, List, Optional, Set, Tuple + +import networkx as nx + +from ..data_types.floats import Float +from ..data_types.integers import Integer +from ..operator_graph import OPGraph +from ..representation import intermediate as ir + + +def fuse_float_operations(op_graph: OPGraph): + """Finds and fuses float domains into single Integer to Integer ArbitraryFunction. + + Args: + op_graph (OPGraph): The OPGraph to simplify + """ + + nx_graph = op_graph.graph + processed_terminal_nodes: Set[ir.IntermediateNode] = set() + while True: + float_subgraph_search_result = find_float_subgraph_with_unique_terminal_node( + nx_graph, processed_terminal_nodes + ) + if float_subgraph_search_result is None: + break + + float_subgraph_start_nodes, terminal_node, subgraph_all_nodes = float_subgraph_search_result + processed_terminal_nodes.add(terminal_node) + + subgraph_conversion_result = convert_float_subgraph_to_fused_node( + op_graph, + float_subgraph_start_nodes, + terminal_node, + subgraph_all_nodes, + ) + + # Not a subgraph we can handle, continue + if subgraph_conversion_result is None: + continue + + fused_node, node_before_subgraph = subgraph_conversion_result + + nx_graph.add_node(fused_node, content=fused_node) + + if terminal_node in op_graph.output_nodes.values(): + # Output value replace it + # As the graph changes recreate the output_node_to_idx dict + output_node_to_idx: Dict[ir.IntermediateNode, List[int]] = { + out_node: [] for out_node in op_graph.output_nodes.values() + } + for output_idx, output_node in op_graph.output_nodes.items(): + output_node_to_idx[output_node].append(output_idx) + + for output_idx in output_node_to_idx.get(terminal_node, []): + op_graph.output_nodes[output_idx] = fused_node + + # Disconnect after terminal node and connect fused node instead + terminal_node_succ = list(nx_graph.successors(terminal_node)) + for succ in terminal_node_succ: + succ_edge_data = deepcopy(nx_graph.get_edge_data(terminal_node, succ)) + for edge_key, edge_data in succ_edge_data.items(): + nx_graph.remove_edge(terminal_node, succ, key=edge_key) + nx_graph.add_edge(fused_node, succ, key=edge_key, **edge_data) + + # Connect the node feeding the subgraph contained in fused_node + nx_graph.add_edge(node_before_subgraph, fused_node, input_idx=0) + + op_graph.prune_nodes() + + +def convert_float_subgraph_to_fused_node( + op_graph: OPGraph, + float_subgraph_start_nodes: Set[ir.IntermediateNode], + terminal_node: ir.IntermediateNode, + subgraph_all_nodes: Set[ir.IntermediateNode], +) -> Optional[Tuple[ir.ArbitraryFunction, ir.IntermediateNode]]: + """Converts a float subgraph to an equivalent fused ArbitraryFunction node. + + Args: + op_graph (OPGraph): The OPGraph the float subgraph is part of. + float_subgraph_start_nodes (Set[ir.IntermediateNode]): The nodes starting the float subgraph + in `op_graph`. + terminal_node (ir.IntermediateNode): The node ending the float subgraph. + subgraph_all_nodes (Set[ir.IntermediateNode]): All the nodes in the float subgraph. + + Returns: + Optional[Tuple[ir.ArbitraryFunction, ir.IntermediateNode]]: None if the float subgraph + cannot be fused, otherwise returns a tuple containing the fused node and the node whose + output must be plugged as the input to the subgraph. + """ + + if not subgraph_has_unique_variable_input(float_subgraph_start_nodes): + return None + + # Only one variable input node, find which node feeds its input + non_constant_input_nodes = [ + node for node in float_subgraph_start_nodes if not isinstance(node, ir.ConstantInput) + ] + assert len(non_constant_input_nodes) == 1 + + current_subgraph_variable_input = non_constant_input_nodes[0] + new_input_value = deepcopy(current_subgraph_variable_input.outputs[0]) + + nx_graph = op_graph.graph + + nodes_after_input_set = subgraph_all_nodes.intersection( + nx_graph.succ[current_subgraph_variable_input] + ) + + float_subgraph = nx.MultiDiGraph(nx_graph.subgraph(subgraph_all_nodes)) + + new_subgraph_variable_input = ir.Input(new_input_value, "float_subgraph_input", 0) + float_subgraph.add_node(new_subgraph_variable_input) + + for node_after_input in nodes_after_input_set: + # Connect the new input to our subgraph + edge_data_input_to_subgraph = deepcopy( + float_subgraph.get_edge_data( + current_subgraph_variable_input, + node_after_input, + ) + ) + for edge_key, edge_data in edge_data_input_to_subgraph.items(): + float_subgraph.remove_edge( + current_subgraph_variable_input, node_after_input, key=edge_key + ) + float_subgraph.add_edge( + new_subgraph_variable_input, + node_after_input, + key=edge_key, + **edge_data, + ) + + float_op_subgraph = OPGraph.from_graph( + float_subgraph, + [new_subgraph_variable_input], + [terminal_node], + ) + + # Create fused_node + fused_node = ir.ArbitraryFunction( + deepcopy(new_subgraph_variable_input.inputs[0]), + lambda x, float_op_subgraph, terminal_node: float_op_subgraph.evaluate({0: x})[ + terminal_node + ], + deepcopy(terminal_node.outputs[0].data_type), + op_kwargs={ + "float_op_subgraph": float_op_subgraph, + "terminal_node": terminal_node, + }, + op_name="Subgraph", + ) + + return ( + fused_node, + current_subgraph_variable_input, + ) + + +def find_float_subgraph_with_unique_terminal_node( + nx_graph: nx.MultiDiGraph, + processed_terminal_nodes: Set[ir.IntermediateNode], +) -> Optional[Tuple[Set[ir.IntermediateNode], ir.IntermediateNode, Set[ir.IntermediateNode]]]: + """Find a subgraph of the graph with float computations. + + The subgraph has a single terminal node with a single Integer output and has a single variable + predecessor node with a single Integer output. + + Args: + nx_graph (nx.MultiDiGraph): The networkx graph to search in. + processed_terminal_nodes (Set[ir.IntermediateNode]): The set of terminal nodes for which + subgraphs have already been searched, those will be skipped. + + Returns: + Optional[Tuple[Set[ir.IntermediateNode], ir.IntermediateNode, Set[ir.IntermediateNode]]]: + None if there are no float subgraphs to process in `nx_graph`. Otherwise returns a tuple + containing the set of nodes beginning a float subgraph, the terminal node of the + subgraph and the set of all the nodes in the subgraph. + """ + + def is_float_to_single_int_node(node: ir.IntermediateNode) -> bool: + return ( + any(isinstance(input_.data_type, Float) for input_ in node.inputs) + and len(node.outputs) == 1 + and isinstance(node.outputs[0].data_type, Integer) + ) + + def single_int_output_node(node: ir.IntermediateNode) -> bool: + return len(node.outputs) == 1 and isinstance(node.outputs[0].data_type, Integer) + + float_subgraphs_terminal_nodes = ( + node + for node in nx_graph.nodes() + if is_float_to_single_int_node(node) and node not in processed_terminal_nodes + ) + + terminal_node: ir.IntermediateNode + + try: + terminal_node = next(float_subgraphs_terminal_nodes) + except StopIteration: + return None + + # Use dict as ordered set + current_nodes = {terminal_node: None} + float_subgraph_start_nodes: Set[ir.IntermediateNode] = set() + subgraph_all_nodes: Set[ir.IntermediateNode] = set() + while current_nodes: + next_nodes: Dict[ir.IntermediateNode, None] = dict() + for node in current_nodes: + subgraph_all_nodes.add(node) + predecessors = nx_graph.pred[node] + for pred in predecessors: + if single_int_output_node(pred): + # Limit of subgraph, record that and record the node as we won't visit it + float_subgraph_start_nodes.add(pred) + subgraph_all_nodes.add(pred) + else: + next_nodes.update({pred: None}) + current_nodes = next_nodes + + return float_subgraph_start_nodes, terminal_node, subgraph_all_nodes + + +def subgraph_has_unique_variable_input( + float_subgraph_start_nodes: Set[ir.IntermediateNode], +) -> bool: + """Check that only one of the nodes starting the subgraph is variable. + + Args: + float_subgraph_start_nodes (Set[ir.IntermediateNode]): The nodes starting the subgraph. + + Returns: + bool: True if only one of the nodes is not an ir.ConstantInput + """ + # Only one input to the subgraph where computations are done in floats is variable, this + # is the only case we can manage with ArbitraryFunction fusing + return sum(not isinstance(node, ir.ConstantInput) for node in float_subgraph_start_nodes) == 1 diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 89eb79713..153cc076c 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -1,8 +1,9 @@ """hnumpy compilation function.""" -from typing import Any, Callable, Dict, Iterator, Optional, Tuple +from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset +from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts from ..common.data_types import BaseValue from ..common.mlir.utils import ( @@ -10,6 +11,8 @@ from ..common.mlir.utils import ( update_bit_width_for_mlir, ) from ..common.operator_graph import OPGraph +from ..common.optimization.topological import fuse_float_operations +from ..common.representation import intermediate as ir from ..hnumpy.tracing import trace_numpy_function @@ -38,6 +41,20 @@ def compile_numpy_function( # Trace op_graph = trace_numpy_function(function_to_trace, function_parameters) + # Fuse float operations to have int to int ArbitraryFunction + if not check_op_graph_is_integer_program(op_graph): + fuse_float_operations(op_graph) + + # TODO: To be removed once we support more than integers + offending_non_integer_nodes: List[ir.IntermediateNode] = [] + op_grap_is_int_prog = check_op_graph_is_integer_program(op_graph, offending_non_integer_nodes) + if not op_grap_is_int_prog: + raise ValueError( + f"{function_to_trace.__name__} cannot be compiled as it has nodes with either float " + f"inputs or outputs.\nOffending nodes : " + f"{', '.join(str(node) for node in offending_non_integer_nodes)}" + ) + # Find bounds with the dataset node_bounds = eval_op_graph_bounds_on_dataset(op_graph, dataset) diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py new file mode 100644 index 000000000..046cfdbdb --- /dev/null +++ b/tests/common/optimization/test_float_fusing.py @@ -0,0 +1,107 @@ +"""Test file for float subgraph fusing""" + +from inspect import signature + +import numpy +import pytest + +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.common.optimization.topological import fuse_float_operations +from hdk.hnumpy.tracing import trace_numpy_function + + +def no_fuse(x): + """No fuse""" + return x + 2 + + +def no_fuse_unhandled(x, y): + """No fuse unhandled""" + x_1 = x + 0.7 + y_1 = y + 1.3 + intermediate = x_1 + y_1 + return intermediate.astype(numpy.int32) + + +def simple_fuse_not_output(x): + """Simple fuse not output""" + intermediate = x.astype(numpy.float64) + intermediate = intermediate.astype(numpy.int32) + return intermediate + 2 + + +def simple_fuse_output(x): + """Simple fuse output""" + return x.astype(numpy.float64).astype(numpy.int32) + + +def complex_fuse_indirect_input(x, y): + """Complex fuse""" + intermediate = x + y + intermediate = intermediate + 2 + intermediate = intermediate.astype(numpy.float32) + intermediate = intermediate.astype(numpy.int32) + x_p_1 = intermediate + 1.5 + x_p_2 = intermediate + 2.7 + x_p_3 = numpy.rint(x_p_1 + x_p_2) + return ( + x_p_3.astype(numpy.int32), + x_p_2.astype(numpy.int32), + (x_p_2 + 3).astype(numpy.int32), + x_p_3.astype(numpy.int32) + 67, + y, + (y + 4.7).astype(numpy.int32) + 3, + ) + + +def complex_fuse_direct_input(x, y): + """Complex fuse""" + x_p_1 = x + 1.5 + x_p_2 = x + 2.7 + x_p_3 = numpy.rint(x_p_1 + x_p_2) + return ( + x_p_3.astype(numpy.int32), + x_p_2.astype(numpy.int32), + (x_p_2 + 3).astype(numpy.int32), + x_p_3.astype(numpy.int32) + 67, + y, + (y + 4.7).astype(numpy.int32) + 3, + ) + + +@pytest.mark.parametrize( + "function_to_trace,fused", + [ + pytest.param(no_fuse, False, id="no_fuse"), + pytest.param(no_fuse_unhandled, False, id="no_fuse_unhandled"), + pytest.param(simple_fuse_not_output, True, id="no_fuse"), + pytest.param(simple_fuse_output, True, id="no_fuse"), + pytest.param(complex_fuse_indirect_input, True, id="complex_fuse_indirect_input"), + pytest.param(complex_fuse_direct_input, True, id="complex_fuse_direct_input"), + ], +) +@pytest.mark.parametrize("input_", [0, 2, 42, 44]) +def test_fuse_float_operations(function_to_trace, fused, input_): + """Test function for fuse_float_operations""" + + params_names = signature(function_to_trace).parameters.keys() + + op_graph = trace_numpy_function( + function_to_trace, + {param_name: EncryptedValue(Integer(32, True)) for param_name in params_names}, + ) + orig_num_nodes = len(op_graph.graph) + fuse_float_operations(op_graph) + fused_num_nodes = len(op_graph.graph) + + if fused: + assert fused_num_nodes < orig_num_nodes + else: + assert fused_num_nodes == orig_num_nodes + + input_ = numpy.int32(input_) + + num_params = len(params_names) + inputs = (input_,) * num_params + assert function_to_trace(*inputs) == op_graph(*inputs) diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 098967130..e5a077980 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -1,6 +1,7 @@ """Test file for hnumpy compilation functions""" import itertools +import numpy import pytest from hdk.common.data_types.integers import Integer @@ -10,6 +11,14 @@ from hdk.common.extensions.table import LookupTable from hdk.hnumpy.compile import compile_numpy_function +def no_fuse_unhandled(x, y): + """No fuse unhandled""" + x_intermediate = x + 2.8 + y_intermediate = y + 9.3 + intermediate = x_intermediate + y_intermediate + return intermediate.astype(numpy.int32) + + @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ @@ -21,6 +30,12 @@ from hdk.hnumpy.compile import compile_numpy_function ((4, 8), (3, 4), (0, 4)), ["x", "y", "z"], ), + pytest.param( + no_fuse_unhandled, + ((-2, 2), (-2, 2)), + ["x", "y"], + marks=pytest.mark.xfail(raises=ValueError), + ), ], ) def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_names): From 788e94bfa378b7f1795c9ff72987bda575fd9260 Mon Sep 17 00:00:00 2001 From: youben11 Date: Tue, 17 Aug 2021 14:43:01 +0100 Subject: [PATCH 0098/1104] feat: end to end compilation and execution --- .github/workflows/continuous-integration.yaml | 3 ++ benchmarks/test_compilation_and_evaluation.py | 6 +-- examples/QuantizedLinearRegression.ipynb | 4 +- examples/QuantizedLogisticRegression.ipynb | 4 +- hdk/hnumpy/compile.py | 47 +++++++++++++++++-- tests/common/compilation/test_artifacts.py | 4 +- tests/common/mlir/test_mlir_converter.py | 4 +- tests/hnumpy/test_compile.py | 44 +++++++++++++++-- 8 files changed, 96 insertions(+), 20 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 88081520c..f1261fe36 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -60,6 +60,9 @@ jobs: - name: PyTest id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + env: + # TODO: remove this when concrete is statically linked with compiler + LD_PRELOAD: /concrete/target/release/libconcrete_ffi.so run: | make pytest - name: Notebooks diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py index c44e5a396..a7e45d6b3 100644 --- a/benchmarks/test_compilation_and_evaluation.py +++ b/benchmarks/test_compilation_and_evaluation.py @@ -6,7 +6,7 @@ import pytest from hdk.common.data_types.integers import SignedInteger, UnsignedInteger from hdk.common.data_types.values import EncryptedValue -from hdk.hnumpy.compile import compile_numpy_function +from hdk.hnumpy.compile import compile_numpy_function_into_op_graph @pytest.mark.parametrize( @@ -35,7 +35,7 @@ def test_compilation(benchmark, function, parameters, ranges): @benchmark def compilation(): - compile_numpy_function(function, parameters, dataset(ranges)) + compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) @pytest.mark.parametrize( @@ -72,7 +72,7 @@ def test_evaluation(benchmark, function, parameters, ranges, inputs): for prod in itertools.product(*args): yield prod - graph = compile_numpy_function(function, parameters, dataset(ranges)) + graph = compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) @benchmark def evaluation(): diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 1b9aae5b3..52e26535c 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -623,13 +623,13 @@ "source": [ "from hdk.common.data_types.integers import Integer\n", "from hdk.common.data_types.values import EncryptedValue\n", - "from hdk.hnumpy.compile import compile_numpy_function\n", + "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", "for x_i in x_q:\n", " dataset.append((int(x_i[0]),))\n", "\n", - "homomorphic_model = compile_numpy_function(\n", + "homomorphic_model = compile_numpy_function_into_op_graph(\n", " infer,\n", " {\"x_0\": EncryptedValue(Integer(input_bits, is_signed=False))},\n", " iter(dataset),\n", diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 37bac1437..1b1ef297f 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -727,13 +727,13 @@ "source": [ "from hdk.common.data_types.integers import Integer\n", "from hdk.common.data_types.values import EncryptedValue\n", - "from hdk.hnumpy.compile import compile_numpy_function\n", + "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", "for x_i in x_q:\n", " dataset.append((int(x_i[0]), int(x_i[1])))\n", " \n", - "homomorphic_model = compile_numpy_function(\n", + "homomorphic_model = compile_numpy_function_into_op_graph(\n", " infer,\n", " {\n", " \"x_0\": EncryptedValue(Integer(input_bits, is_signed=False)),\n", diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 153cc076c..03f899448 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -2,10 +2,13 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple +from zamalang import CompilerEngine + from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts from ..common.data_types import BaseValue +from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( is_graph_values_compatible_with_mlir, update_bit_width_for_mlir, @@ -16,13 +19,13 @@ from ..common.representation import intermediate as ir from ..hnumpy.tracing import trace_numpy_function -def compile_numpy_function( +def compile_numpy_function_into_op_graph( function_to_trace: Callable, function_parameters: Dict[str, BaseValue], dataset: Iterator[Tuple[Any, ...]], compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: - """Main API of hnumpy, to be able to compile an homomorphic program. + """Compile a function into an OPGraph. Args: function_to_trace (Callable): The function you want to trace @@ -35,8 +38,7 @@ def compile_numpy_function( during compilation Returns: - OPGraph: currently returns a compilable graph, but later, it will return an MLIR compatible - with the compiler, and even later, it will return the result of the compilation + OPGraph: compiled function into a graph """ # Trace op_graph = trace_numpy_function(function_to_trace, function_parameters) @@ -74,3 +76,40 @@ def compile_numpy_function( compilation_artifacts.bounds = node_bounds return op_graph + + +def compile_numpy_function( + function_to_trace: Callable, + function_parameters: Dict[str, BaseValue], + dataset: Iterator[Tuple[Any, ...]], + compilation_artifacts: Optional[CompilationArtifacts] = None, +) -> CompilerEngine: + """Main API of hnumpy, to be able to compile an homomorphic program. + + Args: + function_to_trace (Callable): The function you want to trace + function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the + function is e.g. an EncryptedValue holding a 7bits unsigned Integer + dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + needs to be an iterator on tuples which are of the same length than the number of + parameters in the function, and in the same order than these same parameters + compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill + during compilation + + Returns: + CompilerEngine: engine to run and debug the compiled graph + """ + # Compile into an OPGraph + op_graph = compile_numpy_function_into_op_graph( + function_to_trace, function_parameters, dataset, compilation_artifacts + ) + + # Convert graph to an MLIR representation + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + mlir_result = converter.convert(op_graph) + + # Compile the MLIR representation + engine = CompilerEngine() + engine.compile_fhe(mlir_result) + + return engine diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 5c0f6afc1..6fae76174 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -6,7 +6,7 @@ from pathlib import Path from hdk.common.compilation import CompilationArtifacts from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import EncryptedValue -from hdk.hnumpy.compile import compile_numpy_function +from hdk.hnumpy.compile import compile_numpy_function_into_op_graph def test_artifacts_export(): @@ -16,7 +16,7 @@ def test_artifacts_export(): return x + 42 artifacts = CompilationArtifacts() - compile_numpy_function( + compile_numpy_function_into_op_graph( function, {"x": EncryptedValue(Integer(7, True))}, iter([(-2,), (-1,), (0,), (1,), (2,)]), diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 22528a808..617dd1c52 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -10,7 +10,7 @@ from zamalang.dialects import hlfhe from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter -from hdk.hnumpy.compile import compile_numpy_function +from hdk.hnumpy.compile import compile_numpy_function_into_op_graph def add(x, y): @@ -168,7 +168,7 @@ def datagen(*args): def test_mlir_converter(func, args_dict, args_ranges): """Test the conversion to MLIR by calling the parser from the compiler""" dataset = datagen(*args_ranges) - result_graph = compile_numpy_function(func, args_dict, dataset) + result_graph = compile_numpy_function_into_op_graph(func, args_dict, dataset) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index e5a077980..f03b0e892 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -1,5 +1,6 @@ """Test file for hnumpy compilation functions""" import itertools +import random import numpy import pytest @@ -8,7 +9,10 @@ from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import EncryptedValue from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable -from hdk.hnumpy.compile import compile_numpy_function +from hdk.hnumpy.compile import ( + compile_numpy_function, + compile_numpy_function_into_op_graph, +) def no_fuse_unhandled(x, y): @@ -49,7 +53,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n arg_name: EncryptedValue(Integer(64, True)) for arg_name in list_of_arg_names } - op_graph = compile_numpy_function( + op_graph = compile_numpy_function_into_op_graph( function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), @@ -63,6 +67,36 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n print(f"\n{str_of_the_graph}\n") +@pytest.mark.parametrize( + "function,input_ranges,list_of_arg_names", + [ + pytest.param(lambda x: x + 42, ((0, 2),), ["x"]), + pytest.param(lambda x: x * 2, ((0, 2),), ["x"]), + pytest.param(lambda x: 8 - x, ((0, 2),), ["x"]), + pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), + ], +) +def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_of_arg_names): + """Test function compile_numpy_function for a program with multiple outputs""" + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedValue(Integer(64, False)) for arg_name in list_of_arg_names + } + + compiler_engine = compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + args = [random.randint(low, high) for (low, high) in input_ranges] + compiler_engine.run(*args) + + def test_compile_function_with_direct_tlu(): """Test compile_numpy_function for a program with direct table lookup""" @@ -71,7 +105,7 @@ def test_compile_function_with_direct_tlu(): def function(x): return x + table[x] - op_graph = compile_numpy_function( + op_graph = compile_numpy_function_into_op_graph( function, {"x": EncryptedValue(Integer(2, is_signed=False))}, iter([(0,), (1,), (2,), (3,)]), @@ -90,7 +124,7 @@ def test_compile_function_with_direct_tlu_overflow(): return table[x] with pytest.raises(ValueError): - compile_numpy_function( + compile_numpy_function_into_op_graph( function, {"x": EncryptedValue(Integer(3, is_signed=False))}, iter([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)]), @@ -115,7 +149,7 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): } with pytest.raises(TypeError, match=r"signed integers aren't supported for MLIR lowering"): - compile_numpy_function( + compile_numpy_function_into_op_graph( function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), From 6d14b4b318a7edbc477393de72f2e470a901a87d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 17 Aug 2021 18:09:35 +0200 Subject: [PATCH 0099/1104] fix(build): manage refs for push events --- .github/workflows/continuous-integration.yaml | 4 ++-- script/actions_utils/coverage.sh | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index f1261fe36..f257e30a0 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -8,7 +8,7 @@ on: jobs: build: concurrency: - group: ${{ github.head_ref }} + group: ${{ github.ref }} cancel-in-progress: true runs-on: ubuntu-20.04 @@ -114,7 +114,7 @@ jobs: needs: [build] runs-on: ubuntu-20.04 - if: ${{ github.event_name == 'push' && github.base_ref == 'main' }} + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} steps: - name: Download Documentation diff --git a/script/actions_utils/coverage.sh b/script/actions_utils/coverage.sh index 9fbd08376..216e31d87 100755 --- a/script/actions_utils/coverage.sh +++ b/script/actions_utils/coverage.sh @@ -6,7 +6,12 @@ set +e CURR_DIR=`dirname $0` # Run diff-coverage -BB="origin/$1" make coverage | tee diff-coverage.txt +if [[ "$1" == "" ]]; then + BB="origin/main" +else + BB="origin/$1" +fi +make coverage | tee diff-coverage.txt # Get exit code without closing the script TEST_EXIT_CODE="$?" From ed66981ccdb291973360bf17a7795aac5a4ec487 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 18 Aug 2021 09:30:28 +0200 Subject: [PATCH 0100/1104] chore(reqs): move dev requirements to dev category --- poetry.lock | 82 +++++++++++++++++++++++++------------------------- pyproject.toml | 8 ++--- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/poetry.lock b/poetry.lock index e9ad497d6..1512e87b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,7 +2,7 @@ name = "alabaster" version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -57,7 +57,7 @@ wrapt = ">=1.11,<1.13" name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -65,7 +65,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -79,7 +79,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> name = "babel" version = "2.9.1" description = "Internationalization utilities" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -135,7 +135,7 @@ webencodings = "*" name = "certifi" version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -162,7 +162,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "charset-normalizer" version = "2.0.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" +category = "dev" optional = false python-versions = ">=3.5.0" @@ -185,7 +185,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -249,7 +249,7 @@ toml = ["tomli (>=1.2.1,<2.0.0)"] name = "docutils" version = "0.16" description = "Docutils -- Python Documentation Utilities" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -294,7 +294,7 @@ dev = ["coverage", "black", "hypothesis", "hypothesmith"] name = "idna" version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -302,7 +302,7 @@ python-versions = ">=3.5" name = "imagesize" version = "1.2.0" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -310,7 +310,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "4.6.4" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -339,7 +339,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -453,7 +453,7 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] name = "jinja2" version = "3.0.1" description = "A very fast and expressive template engine." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -598,7 +598,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" name = "markdown-it-py" version = "1.1.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" +category = "dev" optional = false python-versions = "~=3.6" @@ -618,7 +618,7 @@ testing = ["coverage", "psutil", "pytest (>=3.6,<4)", "pytest-benchmark (>=3.2,< name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -661,7 +661,7 @@ python-versions = "*" name = "mdit-py-plugins" version = "0.2.8" description = "Collection of plugins for markdown-it-py" -category = "main" +category = "dev" optional = false python-versions = "~=3.6" @@ -711,7 +711,7 @@ python-versions = "*" name = "myst-parser" version = "0.15.1" description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -877,7 +877,7 @@ python-versions = ">=3.7,<3.11" name = "packaging" version = "21.0" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -951,7 +951,7 @@ python-versions = ">=3.6" name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -995,7 +995,7 @@ python-versions = "*" name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1003,7 +1003,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "py-cpuinfo" version = "8.0.0" description = "Get CPU info with pure Python 2 & 3" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -1064,7 +1064,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pygments" version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -1103,7 +1103,7 @@ python-versions = ">=3.6" name = "pytest" version = "6.2.4" description = "pytest: simple powerful testing with Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1125,7 +1125,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm name = "pytest-benchmark" version = "3.4.1" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -1169,7 +1169,7 @@ six = ">=1.5" name = "pytz" version = "2021.1" description = "World timezone definitions, modern and historical" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -1193,7 +1193,7 @@ python-versions = ">=3.6" name = "pyyaml" version = "5.4.1" description = "YAML parser and emitter for Python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" @@ -1251,7 +1251,7 @@ python-versions = "*" name = "requests" version = "2.26.0" description = "Python HTTP for Humans." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" @@ -1290,7 +1290,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" name = "snowballstemmer" version = "2.1.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -1298,7 +1298,7 @@ python-versions = "*" name = "sphinx" version = "4.1.2" description = "Python documentation generator" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1329,7 +1329,7 @@ test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] name = "sphinx-rtd-theme" version = "0.5.2" description = "Read the Docs theme for Sphinx" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -1344,7 +1344,7 @@ dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -1356,7 +1356,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -1368,7 +1368,7 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1380,7 +1380,7 @@ test = ["pytest", "html5lib"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -1391,7 +1391,7 @@ test = ["pytest", "flake8", "mypy"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -1403,7 +1403,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" +category = "dev" optional = false python-versions = ">=3.5" @@ -1442,7 +1442,7 @@ test = ["pytest", "pathlib2"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -1488,7 +1488,7 @@ python-versions = "*" name = "typing-extensions" version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -1496,7 +1496,7 @@ python-versions = "*" name = "urllib3" version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" @@ -1544,7 +1544,7 @@ python-versions = "*" name = "zipp" version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1555,7 +1555,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "e4c9439ab07d0be04d57d753d399bcca05f00331cd45eee889f732246d4edef4" +content-hash = "65489a7f8c03f8825d0948ffec2ef5809e53d82e5b9eb3c77d5fa512c175d0fd" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 37d500f7e..5c7eb534c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,13 +6,9 @@ authors = ["Arthur Meyre "] [tool.poetry.dependencies] python = ">=3.7,<3.10" -Sphinx = "^4.1.1" -sphinx-rtd-theme = "^0.5.2" -myst-parser = "^0.15.1" networkx = "^2.6.1" matplotlib = "^3.4.2" numpy = "^1.21.1" -pytest-benchmark = "^3.4.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" @@ -20,6 +16,7 @@ black = "21.7b0" pylint = "^2.9.3" pytest = "^6.2.4" pytest-cov = "^2.12.1" +pytest-benchmark = "^3.4.1" diff-cover = "^6.2.0" mypy = "^0.910" pydocstyle = "^6.1.1" @@ -27,6 +24,9 @@ jupyter = "^1.0.0" nbmake = "^0.5" flake8 = "^3.9.2" flake8-bugbear = "^21.4.3" +Sphinx = "^4.1.1" +sphinx-rtd-theme = "^0.5.2" +myst-parser = "^0.15.1" [build-system] requires = ["poetry-core>=1.0.0"] From 2dda00aeef78928696022314e38e7a33db329b21 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 18 Aug 2021 11:19:25 +0300 Subject: [PATCH 0101/1104] chore(Makefile): add -s flag to pytest to improve the output of compiler tests --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index df67344ca..688e83858 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ pcc_internal: check_python_format python_linting mypy_ci pydocstyle .PHONY: pcc_internal pytest: - poetry run pytest --cov=hdk -vv --cov-report=xml tests/ + poetry run pytest -svv --cov=hdk --cov-report=xml tests/ .PHONY: pytest # Not a huge fan of ignoring missing imports, but some packages do not have typing stubs From c79e232d0fc242bcacec98293791f064df722d31 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 18 Aug 2021 11:19:27 +0300 Subject: [PATCH 0102/1104] fix(Dockerfile): add LD_PRELOAD export to .bashrc of the docker environment --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index dd34963ac..af09e3aa0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,7 +5,8 @@ RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-i pip install --no-cache-dir poetry && \ echo "python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ - echo "cd /hdk/ && make setup_env" >> /root/.bashrc + echo "cd /hdk/ && make setup_env" >> /root/.bashrc && \ + echo "export LD_PRELOAD=/concrete/target/release/libconcrete_ffi.so" >> /root/.bashrc WORKDIR /hdk From 5038502327e3a3743ecd3816e78a796ef9b1163d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 18 Aug 2021 10:44:56 +0200 Subject: [PATCH 0103/1104] chore(test): make printable graph tests less verbose but keep debug info --- tests/hnumpy/test_debugging.py | 36 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 080ad56e3..b29c18cd7 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -141,10 +141,11 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): str_of_the_graph = get_printable_graph(graph) - print(f"\nGot {str_of_the_graph}\n") - print(f"\nExp {ref_graph_str}\n") - - assert str_of_the_graph == ref_graph_str + assert str_of_the_graph == ref_graph_str, ( + f"\n==================\nGot {str_of_the_graph}" + f"\n==================\nExpected {ref_graph_str}" + f"\n==================\n" + ) @pytest.mark.parametrize( @@ -170,10 +171,11 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph str_of_the_graph = get_printable_graph(graph) - print(f"\nGot {str_of_the_graph}\n") - print(f"\nExp {ref_graph_str}\n") - - assert str_of_the_graph == ref_graph_str + assert str_of_the_graph == ref_graph_str, ( + f"\n==================\nGot {str_of_the_graph}" + f"\n==================\nExpected {ref_graph_str}" + f"\n==================\n" + ) # Remark that the bitwidths are not particularly correct (eg, a MUL of a 17b times 23b @@ -213,10 +215,11 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): str_of_the_graph = get_printable_graph(graph, show_data_types=True) - print(f"\nGot {str_of_the_graph}\n") - print(f"\nExp {ref_graph_str}\n") - - assert str_of_the_graph == ref_graph_str + assert str_of_the_graph == ref_graph_str, ( + f"\n==================\nGot {str_of_the_graph}" + f"\n==================\nExpected {ref_graph_str}" + f"\n==================\n" + ) @pytest.mark.parametrize( @@ -258,7 +261,8 @@ def test_hnumpy_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref str_of_the_graph = get_printable_graph(graph, show_data_types=True) - print(f"\nGot {str_of_the_graph}\n") - print(f"\nExp {ref_graph_str}\n") - - assert str_of_the_graph == ref_graph_str + assert str_of_the_graph == ref_graph_str, ( + f"\n==================\nGot {str_of_the_graph}" + f"\n==================\nExpected {ref_graph_str}" + f"\n==================\n" + ) From 5d26ad499a8a8e806d09864921b5ae9f4f1f6c42 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 18 Aug 2021 11:55:41 +0200 Subject: [PATCH 0104/1104] fix(build): properly serialize calls to mypy - add an helper script to serialize make commands - serialize mypy commands as they may overwrite each others cache - format coverage command to avoid being ridiculously long --- Makefile | 11 +++++++---- script/make_utils/serialize_targets.sh | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100755 script/make_utils/serialize_targets.sh diff --git a/Makefile b/Makefile index 688e83858..5a9095b96 100644 --- a/Makefile +++ b/Makefile @@ -64,17 +64,20 @@ mypy_benchmark: find ./benchmarks/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports .PHONY: mypy_benchmark +# The plus indicates that make will be called by the command and allows to share the context with +# the parent make execution. We serialize calls to these targets as they may overwrite each others +# cache which can cause issues. mypy_ci: - @$(MAKE) --no-print-directory mypy - @$(MAKE) --no-print-directory mypy_test - @$(MAKE) --no-print-directory mypy_benchmark + +poetry run env bash script/make_utils/serialize_targets.sh mypy mypy_test mypy_benchmark .PHONY: mypy_ci pytest_and_coverage: pytest coverage .PHONY: pytest_and_coverage coverage: - @if [[ "$$BB" == "" ]]; then BB=origin/main; fi && poetry run diff-cover coverage.xml --fail-under 100 --html-report coverage.html --compare-branch $$BB + @if [[ "$$BB" == "" ]]; then BB=origin/main; fi && \ + poetry run diff-cover coverage.xml --fail-under 100 \ + --html-report coverage.html --compare-branch $$BB .PHONY: coverage docker_build: diff --git a/script/make_utils/serialize_targets.sh b/script/make_utils/serialize_targets.sh new file mode 100755 index 000000000..882bdb6e6 --- /dev/null +++ b/script/make_utils/serialize_targets.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set +e + +EXIT_CODE=0 + +for make_target in "$@"; do + make "${make_target}" + if [[ "$?" != "0" ]]; then + EXIT_CODE=1 + fi +done + +exit "${EXIT_CODE}" From 1b33cd730761f0fa6b0682296c795765a82b590d Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 18 Aug 2021 14:24:51 +0300 Subject: [PATCH 0105/1104] feat(Makefile): enable jupyter notebook support in docker environment --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5a9095b96..75f193c47 100644 --- a/Makefile +++ b/Makefile @@ -90,7 +90,7 @@ docker_rebuild: docker_start: @# the slash before pwd is for Windows - docker run --rm -it --volume /"$$(pwd)":/hdk hdk:mlir + docker run --rm -it -p 8888:8888 --volume /"$$(pwd)":/hdk hdk:mlir .PHONY: docker_start docker_build_and_start: docker_build docker_start @@ -135,3 +135,7 @@ notebook_timeout: benchmark: poetry run pytest benchmarks/ --benchmark-save=findings .PHONY: benchmark + +jupyter: + poetry run jupyter notebook --allow-root --no-browser --ip=0.0.0.0 +.PHONY: jupyter From a367d68c6ee68f64d96ce1c489fe82aa4abff83a Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 16 Aug 2021 16:57:15 +0300 Subject: [PATCH 0106/1104] feat(compilation-configuration): make compilation customizable --- hdk/common/compilation/__init__.py | 1 + hdk/common/compilation/configuration.py | 13 ++++ hdk/hnumpy/compile.py | 38 ++++++++-- tests/common/compilation/test_artifacts.py | 2 +- .../common/compilation/test_configuration.py | 72 +++++++++++++++++++ 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 hdk/common/compilation/configuration.py create mode 100644 tests/common/compilation/test_configuration.py diff --git a/hdk/common/compilation/__init__.py b/hdk/common/compilation/__init__.py index 3dba32973..59e402417 100644 --- a/hdk/common/compilation/__init__.py +++ b/hdk/common/compilation/__init__.py @@ -1,3 +1,4 @@ """Module for compilation related types.""" from .artifacts import CompilationArtifacts +from .configuration import CompilationConfiguration diff --git a/hdk/common/compilation/configuration.py b/hdk/common/compilation/configuration.py new file mode 100644 index 000000000..648a6c9a8 --- /dev/null +++ b/hdk/common/compilation/configuration.py @@ -0,0 +1,13 @@ +"""Module for compilation configuration.""" + + +class CompilationConfiguration: + """Class that allows the compilation process to be customized.""" + + enable_topological_optimizations: bool + + def __init__( + self, + enable_topological_optimizations: bool = True, + ): + self.enable_topological_optimizations = enable_topological_optimizations diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 03f899448..1f629b674 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -6,7 +6,7 @@ from zamalang import CompilerEngine from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from ..common.common_helpers import check_op_graph_is_integer_program -from ..common.compilation import CompilationArtifacts +from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.data_types import BaseValue from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( @@ -23,6 +23,7 @@ def compile_numpy_function_into_op_graph( function_to_trace: Callable, function_parameters: Dict[str, BaseValue], dataset: Iterator[Tuple[Any, ...]], + compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: """Compile a function into an OPGraph. @@ -34,18 +35,30 @@ def compile_numpy_function_into_op_graph( dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters + compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use + during compilation compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill during compilation Returns: OPGraph: compiled function into a graph """ + + # Create default configuration if custom configuration is not specified + compilation_configuration = ( + CompilationConfiguration() + if compilation_configuration is None + else compilation_configuration + ) + # Trace op_graph = trace_numpy_function(function_to_trace, function_parameters) - # Fuse float operations to have int to int ArbitraryFunction - if not check_op_graph_is_integer_program(op_graph): - fuse_float_operations(op_graph) + # Apply topological optimizations if they are enabled + if compilation_configuration.enable_topological_optimizations: + # Fuse float operations to have int to int ArbitraryFunction + if not check_op_graph_is_integer_program(op_graph): + fuse_float_operations(op_graph) # TODO: To be removed once we support more than integers offending_non_integer_nodes: List[ir.IntermediateNode] = [] @@ -82,6 +95,7 @@ def compile_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue], dataset: Iterator[Tuple[Any, ...]], + compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> CompilerEngine: """Main API of hnumpy, to be able to compile an homomorphic program. @@ -93,15 +107,29 @@ def compile_numpy_function( dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters + compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use + during compilation compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill during compilation Returns: CompilerEngine: engine to run and debug the compiled graph """ + + # Create default configuration if custom configuration is not specified + compilation_configuration = ( + CompilationConfiguration() + if compilation_configuration is None + else compilation_configuration + ) + # Compile into an OPGraph op_graph = compile_numpy_function_into_op_graph( - function_to_trace, function_parameters, dataset, compilation_artifacts + function_to_trace, + function_parameters, + dataset, + compilation_configuration, + compilation_artifacts, ) # Convert graph to an MLIR representation diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 6fae76174..0e2c75697 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -20,7 +20,7 @@ def test_artifacts_export(): function, {"x": EncryptedValue(Integer(7, True))}, iter([(-2,), (-1,), (0,), (1,), (2,)]), - artifacts, + compilation_artifacts=artifacts, ) with tempfile.TemporaryDirectory() as tmp: diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py new file mode 100644 index 000000000..0280fe2a7 --- /dev/null +++ b/tests/common/compilation/test_configuration.py @@ -0,0 +1,72 @@ +"""Test file for compilation configuration""" + +from inspect import signature + +import numpy +import pytest + +from hdk.common.compilation import CompilationConfiguration +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import EncryptedValue +from hdk.hnumpy.compile import compile_numpy_function_into_op_graph + + +def no_fuse(x): + """No fuse""" + return x + 2 + + +def simple_fuse_not_output(x): + """Simple fuse not output""" + intermediate = x.astype(numpy.float64) + intermediate = intermediate.astype(numpy.uint32) + return intermediate + 2 + + +@pytest.mark.parametrize( + "function_to_trace,fused", + [ + pytest.param( + no_fuse, + False, + id="no_fuse", + ), + pytest.param( + simple_fuse_not_output, + True, + id="simple_fuse_not_output", + marks=pytest.mark.xfail(strict=True), + # fails because it connot be compiled without topological optimizations + ), + ], +) +def test_enable_topological_optimizations(test_helpers, function_to_trace, fused): + """Test function for enable_topological_optimizations flag of compilation configuration""" + + op_graph = compile_numpy_function_into_op_graph( + function_to_trace, + { + param: EncryptedValue(Integer(32, is_signed=False)) + for param in signature(function_to_trace).parameters.keys() + }, + iter([(1,), (2,), (3,)]), + ) + op_graph_not_optimized = compile_numpy_function_into_op_graph( + function_to_trace, + { + param: EncryptedValue(Integer(32, is_signed=False)) + for param in signature(function_to_trace).parameters.keys() + }, + iter([(1,), (2,), (3,)]), + compilation_configuration=CompilationConfiguration(enable_topological_optimizations=False), + ) + + graph = op_graph.graph + not_optimized_graph = op_graph_not_optimized.graph + + if fused: + assert not test_helpers.digraphs_are_equivalent(graph, not_optimized_graph) + assert len(graph) < len(not_optimized_graph) + else: + assert test_helpers.digraphs_are_equivalent(graph, not_optimized_graph) + assert len(graph) == len(not_optimized_graph) From 0c5d5b0fd0641af428432514afce63c7d419ac03 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 18 Aug 2021 14:18:15 +0200 Subject: [PATCH 0107/1104] docs: change ordering of attributes and methods to be grouped - alphabetical makes the docs hard to read --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index fef124892..fafee81ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,9 @@ templates_path = ["_templates"] # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +# Group member variables and methods separately (not alphabetically) +autodoc_member_order = "groupwise" + # -- Options for HTML output ------------------------------------------------- From 94fef5e202933937e15daae7cd8601bce27e467a Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 18 Aug 2021 16:28:31 +0300 Subject: [PATCH 0108/1104] feat(debugging): provide a way to export the drawn graph as a png file --- Makefile | 2 +- examples/QuantizedLinearRegression.ipynb | 38 ++---- examples/QuantizedLogisticRegression.ipynb | 88 ++++++-------- hdk/common/compilation/artifacts.py | 9 +- hdk/common/debugging/__init__.py | 3 +- .../debugging/{draw_graph.py => drawing.py} | 114 +++++------------- hdk/common/debugging/printing.py | 91 ++++++++++++++ script/nbmake_utils/notebook_sanitize.py | 1 + tests/common/compilation/test_artifacts.py | 1 + 9 files changed, 178 insertions(+), 169 deletions(-) rename hdk/common/debugging/{draw_graph.py => drawing.py} (71%) create mode 100644 hdk/common/debugging/printing.py diff --git a/Makefile b/Makefile index 75f193c47..d4b43eb19 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ flake8: python_linting: pylint flake8 .PHONY: python_linting -conformance: python_format +conformance: strip_nb python_format .PHONY: conformance pcc: diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 52e26535c..8703a6396 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -93,7 +93,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAD4CAYAAAAXUaZHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAW5ElEQVR4nO3dfZBddX3H8feHp4iKXCArjUnaoMY62BlDusY4WIuhKqBjcMY66ViJlE7UiZ3L6iigM3WdKTPi07pMW5xolKAUpIglw+BUhFDHPwi9gRASImUVMImBrMJGKdPUsN/+cX7bnF324d69T2fPfl4zd/ac3zl397s3uZ89+93zO0cRgZmZlctx3S7AzMxaz+FuZlZCDnczsxJyuJuZlZDD3cyshE7odgEACxcujGXLlnW7DDOzOWXHjh2/joieybYVItyXLVtGrVbrdhlmZnOKpCen2ua2jJlZCTnczcxKyOFuZlZCDnczsxJyuJuZdcPE63q1+DpfDnczs07r74e+vmOBHpGt9/e37Es43M3MOikCRkZgcPBYwPf1ZesjIy07gi/Eee5mZvOGBAMD2fLgYPYAqFazcak1X6YI13Pv7e0NT2Iys3klAo7LNU9GRxsOdkk7IqJ3sm1uy5iZddpYKyYv34NvAYe7mVkn5Xvs1Wp2xF6tju/Bt4B77mZmnSRBpTK+xz7Wg69U3HM3M5vTIsYH+cT1OrjnbmZWNBODvEVH7GMc7mY2f7V5lmg31RXukp6Q9LCknZJqaex0SXdJeix9PC2NS9K1koYk7ZK0sp3fgJnZrHRglmg3NXLk/vaIWJHr71wJ3B0Ry4G70zrAhcDy9NgAXNeqYs3MWqJDs0S7qZmzZdYC56XlLcC9wBVp/IbI/lJ7n6SKpEURcbCZQs3MWqZDs0S7qd4j9wB+JGmHpA1p7MxcYD8FnJmWFwP7cs/dn8bGkbRBUk1SbXh4eBalm5k1IR/wY0oS7FB/uL81IlaStVw2SnpbfmM6Sm/o95iI2BQRvRHR29Mz6f1dzczapwOzRLuprnCPiAPp4yHgB8Aq4GlJiwDSx0Np9wPA0tzTl6QxM7Ni6NAs0W6aMdwlvUzSKWPLwDuB3cBWYH3abT1we1reClySzppZDRx2v93MCmWqWaLVaktniXbTjDNUJb2a7Ggdsj/A/ktEXC3pDOAW4A+BJ4EPRMQzkgT8I3AB8DxwaURMO/3UM1TNrCtaMEu0m6aboTrj2TIR8QvgjZOM/wY4f5LxADbOok4zs85q8yzRbvIMVTOzEnK4m5mVkMPdzKyEHO5mZiXkcDczKyGHu5lZCTnczcxKyOFuZlZCDnczsxJyuJuZlZDD3cyshBzuZmYl5HA3Myshh7uZWQk53M3MSsjhbmZWQnWHu6TjJT0o6Y60fr2kxyXtTI8VaVySrpU0JGmXpJVtqt3MzKYw452YcqrAXuAVubFPRcStE/a7EFieHm8GrksfzcysQ+o6cpe0BHg38M06dl8L3BCZ+4CKpEVN1GhmZg2qty3zNeDTwOiE8atT62VA0oI0thjYl9tnfxobR9IGSTVJteHh4QbLNjOz6cwY7pLeAxyKiB0TNl0FvB54E3A6cEUjXzgiNkVEb0T09vT0NPJUMzObQT1H7ucC75X0BHAzsEbSdyPiYGq9HAG+DaxK+x8AluaevySNmZlZh8wY7hFxVUQsiYhlwDrgnoj467E+uiQBFwO701O2Apeks2ZWA4cj4mBbqjczs0k1crbMRDdK6gEE7AQ+msbvBC4ChoDngUubKdDMzBrXULhHxL3AvWl5zRT7BLCx2cLMzGz2PEPVzKyEHO5mZiXkcDczKyGHu5lZCTnczcxKyOFuZo2LmH7dus7hbmaN6e+Hvr5jgR6Rrff3d7Mqm8Dhbmb1i4CRERgcPBbwfX3Z+siIj+ALpJkZqmY230gwMJAtDw5mD4BqNRuXulebjaMowE/a3t7eqNVq3S7DzOoVAcflfvEfHXWwd4GkHRHRO9k2t2XMrDFjrZi8fA/eCsHhbmb1y/fYq9XsiL1aHd+Dt0Jwz93M6idBpTK+xz7Wg69U3JopEPfczaxxEeODfOK6dYR77mbWWhOD3MFeOHWHu6TjJT0o6Y60fpak7ZKGJH1P0klpfEFaH0rbl7WpdrP5zbNEbRqNHLlXgb259WuAgYh4LfAscFkavwx4No0PpP3MrJU8S9RmUFe4S1oCvBv4ZloXsAa4Ne2yhew+qgBr0zpp+/lpfzNrBc8StTrUe7bM14BPA6ek9TOAkYg4mtb3A4vT8mJgH0BEHJV0OO3/61YUbDbveZao1WHGI3dJ7wEORcSOVn5hSRsk1STVhoeHW/mpzcovH/BjHOyWU09b5lzgvZKeAG4ma8cMAhVJY0f+S4ADafkAsBQgbT8V+M3ETxoRmyKiNyJ6e3p6mvomzOYdzxK1GcwY7hFxVUQsiYhlwDrgnoj4ILANeH/abT1we1remtZJ2++JIpxMb1YWniVqdWhmhuoVwM2S/gF4ENicxjcD35E0BDxD9gPBzFrFs0StDp6hajZXeZbovOcZqmZl5FmiNg2Hu5lZCTnczcxKyOFuZlZCDnczsxJyuJuZlZDD3cyshBzuZmYl5HA3Myshh7uZWQk53M3MSsjhbmZWQg53M7MScribmZWQw93MrIQc7mZmJVTPDbJfIul+SQ9J2iPp82n8ekmPS9qZHivSuCRdK2lI0i5JK9v8PZiZ2QT13GbvCLAmIp6TdCLwU0k/TNs+FRG3Ttj/QmB5erwZuC59NDOzDqnnBtkREc+l1RPTY7p7860FbkjPuw+oSFrUfKlmZlavunruko6XtBM4BNwVEdvTpqtT62VA0oI0thjYl3v6/jQ28XNukFSTVBseHp79d2BmZi9SV7hHxAsRsQJYAqyS9CfAVcDrgTcBpwNXNPKFI2JTRPRGRG9PT09jVZuZ2bQaOlsmIkaAbcAFEXEwtV6OAN8GVqXdDgBLc09bksbMzKxD6jlbpkdSJS2fDLwD+NlYH12SgIuB3ekpW4FL0lkzq4HDEXGwDbWbmdkU6jlbZhGwRdLxZD8MbomIOyTdI6kHELAT+Gja/07gImAIeB64tOVVm5nZtGYM94jYBZwzyfiaKfYPYGPzpZmZ2Wx5hqqZWQk53M3MSsjhbmZWQg53s2ZFTL9u1gUOd7Nm9PdDX9+xQI/I1vv7u1mVmcPdbNYiYGQEBgePBXxfX7Y+MuIjeOuqes5zN7PJSDAwkC0PDmYPgGo1G5e6V5vNe4oCHF309vZGrVbrdhlmsxMBx+V+CR4ddbBbR0jaERG9k21zW8asGWOtmLx8D96sSxzuZrOV77FXq9kRe7U6vgdv1iXuuZvNlgSVyvge+1gPvlJxa8a6yj13s2ZFjA/yietmbeKeu1k7TQxyB7sVgMPdzKyEHO5mZiXkcDczK6F6brP3Ekn3S3pI0h5Jn0/jZ0naLmlI0vcknZTGF6T1obR9WZu/BzMzm6CeI/cjwJqIeCOwArgg3Rv1GmAgIl4LPAtclva/DHg2jQ+k/cxmx1dcNJuVGcM9Ms+l1RPTI4A1wK1pfAvZTbIB1qZ10vbz0020zRrjKy6azVpdPXdJx0vaCRwC7gJ+DoxExNG0y35gcVpeDOwDSNsPA2dM8jk3SKpJqg0PDzf1TVgJ+YqLZk2pa4ZqRLwArJBUAX4AvL7ZLxwRm4BNkE1iavbzWcn4iotmTWnobJmIGAG2AW8BKpLGfjgsAQ6k5QPAUoC0/VTgN60o1uaZfMCPcbCb1aWes2V60hE7kk4G3gHsJQv596fd1gO3p+WtaZ20/Z4owjUObO7xFRfNZq2eI/dFwDZJu4D/BO6KiDuAK4BPSBoi66lvTvtvBs5I458Armx92VZ6vuKiWVNm7LlHxC7gnEnGfwGsmmT8f4C/bEl1Nn/5iotmTfFVIa3YfMVFsyn5qpA2d/mKi2az4nA3Myshh7uZWQk53M3MSsjhbmZWQg53ay1fxdGsEBzu1jq+iqNZYTjcrTV8FUezQqnrqpBmM/JVHM0KxTNUrbUi4LjcL4Sjow52szbxDFXrDF/F0awwHO7WGr6Ko1mhuOdureGrOJoVinvu1lq+iqNZx7jnbp3jqziaFUI9t9lbKmmbpEck7ZFUTeP9kg5I2pkeF+Wec5WkIUmPSnpXO78BMzN7sXp67keBT0bEA5JOAXZIuittG4iIL+d3lnQ2sA54A/Aq4MeSXhcRL7SycDMzm9qMR+4RcTAiHkjLvyO7OfbiaZ6yFrg5Io5ExOPAEJPcjs/MzNqnoZ67pGVk91PdnoY+LmmXpG9JOi2NLQb25Z62n0l+GEjaIKkmqTY8PNx45WZmNqW6w13Sy4HvA5dHxG+B64DXACuAg8BXGvnCEbEpInojorenp6eRp5qZ2QzqCndJJ5IF+40RcRtARDwdES9ExCjwDY61Xg4AS3NPX5LGzMysQ+o5W0bAZmBvRHw1N74ot9v7gN1peSuwTtICSWcBy4H7W1eymZnNpJ6zZc4FPgQ8LGlnGvsM8FeSVgABPAF8BCAi9ki6BXiE7EybjT5Txsyss2YM94j4KTDZTJQ7p3nO1cDVTdRlZmZN8AxVM7MScribmZWQw93MrIQc7mZmJeRwn0smXp65AJdrNrNicrjPFf394+9oNHbno/7+blZlZgXlcJ8LImBkZPwt68ZuaTcy4iN4M3sR32ZvLsjfsm5wMHvA+FvamZnl+DZ7c0kEHJf7ZWt01MFuNo/5NntlMNaKycv34M3Mchzuc0G+x16tZkfs1er4HryZWY577nOBBJXK+B77WA++UnFrxsxexD33uSRifJBPXDezecU997KYGOQOdjObgsPdzKyE6rkT01JJ2yQ9ImmPpGoaP13SXZIeSx9PS+OSdK2koXTz7JXt/ibMzGy8eo7cjwKfjIizgdXARklnA1cCd0fEcuDutA5wIdmt9ZYDG8hupG1mZh00Y7hHxMGIeCAt/w7YCywG1gJb0m5bgIvT8lrghsjcB1Qm3G/VzMzarKGeu6RlwDnAduDMiDiYNj0FnJmWFwP7ck/bn8Ymfq4NkmqSasPDw43WbWZm06g73CW9HPg+cHlE/Da/LbLzKRs6pzIiNkVEb0T09vT0NPJUMzObQV3hLulEsmC/MSJuS8NPj7Vb0sdDafwAsDT39CVpzMzMOqSes2UEbAb2RsRXc5u2AuvT8nrg9tz4JemsmdXA4Vz7xszMOqCeyw+cC3wIeFjSzjT2GeALwC2SLgOeBD6Qtt0JXAQMAc8Dl7ayYDMzm9mM4R4RPwWmmgp5/iT7B7CxybrMzKwJnqFqZlZCDnczsxJyuJuZlZDD3cyshBzuZmYl5HA3Myshh7uZWQk53M3MSsjhbmZWQg53M7MScribmZWQw93MrIQc7mZmJeRwNzMrIYe7mVkJOdzNzEqontvsfUvSIUm7c2P9kg5I2pkeF+W2XSVpSNKjkt7VrsLNzGxq9Ry5Xw9cMMn4QESsSI87ASSdDawD3pCe88+Sjm9VsWZmVp8Zwz0ifgI8U+fnWwvcHBFHIuJxsvuormqiPjMzm4Vmeu4fl7QrtW1OS2OLgX25ffansReRtEFSTVJteHi4iTLMzGyi2Yb7dcBrgBXAQeArjX6CiNgUEb0R0dvT0zPLMszMbDKzCveIeDoiXoiIUeAbHGu9HACW5nZdksbMzKyDZhXukhblVt8HjJ1JsxVYJ2mBpLOA5cD9zZVoZmaNOmGmHSTdBJwHLJS0H/gccJ6kFUAATwAfAYiIPZJuAR4BjgIbI+KFtlRuZmZTUkR0uwZ6e3ujVqt1uwwzszlF0o6I6J1sm2eompmVkMPdzKyEHO5mZiXkcDczKyGHu5lZCc3dcJ94lk8BzvoxMyuKuRnu/f3Q13cs0COy9f7+blZlZlYYcy/cI2BkBAYHjwV8X1+2PjLiI3gzM+qYoVo4EgwMZMuDg9kDoFrNxqXu1WZmVhBzd4ZqBByX+8VjdNTBbmbzSvlmqI61YvLyPXgzs3lu7oV7vsderWZH7NXq+B68mdk8Nzd77pXK+B77WA++UnFrxsyMud5zzwf5xHUzs5IrX88dXhzkDnYzs/83d8PdzMymNGO4S/qWpEOSdufGTpd0l6TH0sfT0rgkXStpSNIuSSvbWbyZmU2uniP364ELJoxdCdwdEcuBu9M6wIVk901dDmwArmtNmWZm1ogZwz0ifgI8M2F4LbAlLW8BLs6N3xCZ+4DKhJtpm5lZB8z2VMgzI+JgWn4KODMtLwb25fbbn8YOMoGkDWRH9wDPSXp0lrW0w0Lg190uYhpFrw+KX2PR6wPX2ApFrw+aq/GPptrQ9HnuERGSGj6fMiI2AZua/frtIKk21elFRVD0+qD4NRa9PnCNrVD0+qB9Nc72bJmnx9ot6eOhNH4AWJrbb0kaMzOzDpptuG8F1qfl9cDtufFL0lkzq4HDufaNmZl1yIxtGUk3AecBCyXtBz4HfAG4RdJlwJPAB9LudwIXAUPA88Clbai5EwrZLsopen1Q/BqLXh+4xlYoen3QphoLcfkBMzNrLc9QNTMrIYe7mVkJzftwl1SRdKukn0naK+ktU11eoYs19knaI2m3pJskvUTSWZK2p0s9fE/SSR2uqdCXpZiivi+lf+ddkn4gqZLbdlWq71FJ72p3fVPVmNv2SUkhaWFaL8RrmMb/Lr2OeyR9MTdeiNdQ0gpJ90naKakmaVUa78ZruFTSNkmPpNermsbb/16JiHn9IJth+7dp+SSgAnwRuDKNXQlc08X6FgOPAyen9VuAD6eP69LY14GPdbiutwErgd25sUlfN7I/sv8QELAa2N6l+t4JnJCWr8nVdzbwELAAOAv4OXB8N2pM40uBfyc7WWFhwV7DtwM/Bhak9VcW7TUEfgRcmHvd7u3ia7gIWJmWTwH+K71WbX+vzOsjd0mnkv3n2AwQEf8bESNMfXmFbjkBOFnSCcBLyWb8rgFuTds7XmMU/LIUk9UXET+KiKNp9T6yeRhj9d0cEUci4nGys71WtbO+qWpMBoBPA/mzHQrxGgIfA74QEUfSPmNzXIr0GgbwirR8KvCrXI2dfg0PRsQDafl3wF6yA7a2v1fmdbiTHWEMA9+W9KCkb0p6GVNfXqHjIuIA8GXgl2ShfhjYAYzkgmrsMg/d1uhlKbrpb8iOkKBA9UlaCxyIiIcmbCpKja8D/iy1BP9D0pvSeFHqA7gc+JKkfWTvnavSeFdrlLQMOAfYTgfeK/M93E8g+5Xuuog4B/hvjl3hEsgur8D4I6iOSr24tWQ/iF4FvIwXX6WzcLr9uk1H0meBo8CN3a4lT9JLgc8Af9/tWqZxAnA6WcvgU2TzXYp2p5yPAX0RsRToI/1m3k2SXg58H7g8In6b39au98p8D/f9wP6I2J7WbyUL+6kur9ANfwE8HhHDEfF74DbgXLJf18YmoRXlMg+FvyyFpA8D7wE+mN5UUJz6XkP2Q/whSU+kOh6Q9AcUp8b9wG2pbXA/MEp24aui1AfZrPnb0vK/cqw91JUaJZ1IFuw3RsRYXW1/r8zrcI+Ip4B9kv44DZ0PPMLUl1fohl8CqyW9NB0hjdW4DXh/2qfbNY4p9GUpJF1A1st+b0Q8n9u0FVgnaYGks8juR3B/p+uLiIcj4pURsSwilpEF6cr0/7QQryHwb2R/VEXS68hOQvg1BXkNk18Bf56W1wCPpeWOv4bpPbsZ2BsRX81tav97pd1/LS76A1gB1IBdZP9xTwPOILsJyWNkZwac3uUaPw/8DNgNfIfsjIRXk715hsiOThZ0uKabyP4G8HuyELpsqteN7C///0R2BsXDQG+X6hsi62fuTI+v5/b/bKrvUdKZFt2occL2Jzh2tkxRXsOTgO+m/4sPAGuK9hoCbyX7u9RDZP3tP+3ia/hWspbLrtz/u4s68V7x5QfMzEpoXrdlzMzKyuFuZlZCDnczsxJyuJuZlZDD3cyshBzuZmYl5HA3Myuh/wPi+D/An9GdTgAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -199,7 +199,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAD4CAYAAAAXUaZHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAhW0lEQVR4nO3de5xV8/7H8deHcOTSuMRJIY4cSkoNco1yK5c4l06cc4SSSGcat8o5P8ZduUzDSUQdxRFCuqiUVITKdL9JlxPKpdAFkab5/v74rs2eaaaZaWbPWnvv9/Px2I9Z+7vXzHzaj+3jM5/1Xd+vOecQEZHUskvYAYiISNVTchcRSUFK7iIiKUjJXUQkBSm5i4ikoBphBwBw4IEHuvr164cdhohIUpk1a9bXzrnaJb0WieRev3598vPzww5DRCSpmNknpb2mtoyISApSchcRSUFK7iIiKUjJXUQkBSm5i4ikICV3EZEUpOQuIpKClNxFREKweTP07AmflDpTvXKU3EVEqtnkydC4MfTtC2PHJuZ3KLmLiFSTDRugSxdo1Qp22QWmTIHrr0/M71JyFxGpBqNGQaNGMGgQ3HYbzJ8PLVsm7vcpuYuIJNDatdChA7RrBwceCDNmQJ8+sOeeif29Su4iIgngHDz/PBx7LIwYAffcA/n5kJlZPb8/EqtCioikks8+g65d/cXSFi18K6Zhw+qNQZW7iEgVKSyEAQN8b33KFOjXD6ZNq/7EDqrcRUSqxLJl0LkzvPMOnHMODBwIRxwRXjyq3EVEKqGgwM9XP/54mDfPt2AmTAg3sYMqdxGRnTZvHnTqBLNmwaWXQv/+cMghYUflqXIXEamgLVvg//7Pz3z57DN4+WV47bXoJHZQ5S4iUiEffOCr9SVL4Mor4dFH4YADwo5qe6rcRUTK4fvvoUcPOO00fzx2LAwZEs3EDqrcRUTKNHGiXxNm1SrodoPjgQeNffYJXnQOzMIMr0Sq3EVESrF+PVxzDZx3Huy+O7xz1WD+vVs2++zt/AnOQXY25OSEGmdJlNxFREowYoS/+WjoUL/u+tw5jjNqzYe8PJ/QY4k9L88v9+hc2CEXobaMiEicL7+E7t3hlVegaVN44w1o1gzAIDfXn5SX5x8AWVl+PGKtGVXuIiL4wnvoUF+tjx4N998PM2fGEnvA4hJ8TAQTOyi5i4jw6afQti107OhXcZw7F3r3ht12K3ZirBUTL9aiiRgldxFJW4WF/q7SRo3g3Xfh8cf912OOKeHk+B57Vpb/5qysoj34CFHPXUTS0tKlfqGvadPg/PPhqafg8MN38A1mkJFRtMcea9FkZESuNWMuAv+3yczMdPn5+WGHISLJqvhc8x3MPd+6FR55xM9erFnT32HasWMFcnMFfleimdks51yJ23+Uqy1jZqvMbIGZzTWz/GBsfzObaGbLgq/7BeNmZo+Z2XIzm29mzXb800VEKiEnp2hbZAdzz+fMgZNP9v30iy+GxYvhqqsqmJuLnxyxij2mIj33s51zTeP+L9ELmOScawBMCp4DtAEaBI8uwICqClZEpAjn/BzzMuae//QT3H47nHgifP45vPoqDB8Ov/1tqNEnVGV67u2As4LjIcAUoGcwPtT5fs90M8swszrOuS8qE6iIyHas7Lnn773nF/pauhSuvtq3ZPbbL7yQq0t5K3cHTDCzWWbWJRg7OC5hfwkcHBzXBT6L+97VwVgRZtbFzPLNLH/dunU7EbqICKXOPf/ue6N7dzjjDL9E74QJMHhweiR2KH9yP9051wzfculmZmfGvxhU6RW6MuucG+icy3TOZdauXbsi3yoi8qsS5p6/+YenOO44R//+/m7TBQvg3HNDii8k5Uruzrk1wde1wAjgJOArM6sDEHxdG5y+Bjg07tvrBWMiIlWr2Nzzb78u5KpjZ3DB612p+d1apr3ryMuDvfcOO9DqV2ZyN7O9zGyf2DFwHrAQGAV0DE7rCIwMjkcBVwazZloAG9VvF5GEiJt7/urpuTRsZDz/8Yn888QJzLnhaU49LZozWapDeS6oHgyMMD/dpwbwgnNuvJl9CLxsZp2AT4D2wfljgbbAcmAzcHWVRy0iEviyaw7dujle+7PRrBmMH280bXIu2HlhhxaqMpO7c24l0KSE8W+A1iWMO6BblUQnIlIK5/xOSNnZ8OOPxoMPws03Q40aAOlbscdo+QERSTqrVvmdkSZO9LNhnnkGjj467KiiRQuHiUjS2LYNHnsMjjvOb1Tdvz9MmaLEXhJV7iKSFJYs8Qt9vf8+tGkDTz4Jhx0WdlTRpcpdRCJt61a47z6/K9JHH8Fzz/ndkZTYd0yVu4hE1qxZfumAefOgfXu/3vpBB4UdVXJQ5S4ikfPjj9Crl1/Bce1av1n1Sy8psVeEKncRiZR33vG99WXL/NeHHvL3KUnFqHIXkUjYtAluuAFatoSCAnjrLXj6aSX2naXkLiKhGzfOT2988kl/U9KCBdB6u1skpSLUlhGR0HzzDfToAc8/Dw0b+mmOLVqEHVVqUOUuItXOOXj5ZTj2WHjxRbjjDpg9W4m9KqlyF5Fq9fnnvrc+ciRkZvre+vHHhx1V6lHlLiLVwjkYNMi3X958E/r29UsIKLEnhip3EUm4lSv9Ql+TJvnZMM88A0cdFXZUqU2Vu4gkzLZt0K8fNG4MM2f62TBvv63EXh1UuYtIQixa5JcOmDEDLrzQJ/Z69cKOKn2ocheRKvXzz3D33XDCCbBiBbzwAowercRe3VS5i0iV+fBDX60vWAAdOvi112vXDjuq9KTKXUQqbfNmuPVWP0/9229h1CgYNkyJPUyq3EWkUqZM8Qt8rVgB117rF/qqVSvsqESVu4jslI0b4brr4Oyz/fO334aBA5XYo0LJXUQqbMwYaNTIz1e/+WaYP//XJC/RoOQuIuW2bh1ccQVcfDHst5+/w/Thh6FmzbAjk+KU3EWkTM75C6QNG8Irr0BOjt8C76STwo5MSqMLqiKyQ6tX+4W+Ro/2yXzQIL/2ukSbKncRKco5AAoL/QXSRo0cb70Fjzzi11tXYk8OSu4i8qucHMjOZvkyR+vWfjZM832XsaBzHjfdBLvuGnaAUl5K7iLiOUfBt5t4OK8GjY/dyuzZjoGtX2TS6t/zu13+90tFL8lBPXcRAWDBQqPT9Ef4EOOSbSN5YtMN1J30OWRlQW4umIUdolSAKneRNLdlC9x5JzRrBqtWGS8Oc7zOpdTlc3+CEntSUnIXSWMzZvikfvfdfqGvJYsdf5meTZFUnp2tlkwSUnIXSUM//AA33QSnnAKbNsEbb8BzQx0H3JsNeXm+FVNY6L/m5SnBJyH13EXSzKRJfoGv//3Pz19/4AHYd18Ag4yMoj323Fz/TRkZas0kGSV3kTSxYQPccou/CalBA5g6Fc48s9hJOTm+Qo8l8liCV2JPOmrLiCSD4i2RCrZIRo70Swc8+yz07Anz5pWQ2GOKJ3Il9qRU7uRuZrua2RwzGxM8P8LMZpjZcjN7ycx2D8b3CJ4vD16vn6DYRdJDcGPRLwndOf88J6fMb127Fv7yF7j0Ur9xxowZ8OCDsOeeiQxYoqAilXsWsCTueR8g1zl3FLAe6BSMdwLWB+O5wXkisjOc8/2U+Iua2cFFzw0bSq3gnYP//tdX66+/DvfeC/n50Lx5dQYvoXLOlfkA6gGTgFbAGMCAr4EaweunAG8Gx28CpwTHNYLzbEc/v3nz5k5ESlFY6FxWlnM+Z/tHVpYfL8GnnzrXtq0/rUUL5xYtqtZopRoB+a6UvFreyr0fcBtQGDw/ANjgnCsInq8G6gbHdYHPgv9xFAAbg/OLMLMuZpZvZvnr1q0rZxgiaSh+1kpMCRc5CwthwAC/icaUKb64nzbNV++SfspM7mZ2EbDWOTerKn+xc26gcy7TOZdZW7voipQu1oqJV2ze+ccfw1ln+amNJ58MCxfCP/6hhb7SWXkq99OAS8xsFfAivjWTB2SYWWwqZT1gTXC8BjgUIHi9FvBNFcYskj7ie+wl3FhUsNXRty80aQILFsDgwTBhAhxxRNiBS9jKnOfunOsN9AYws7OAW5xzfzWz4cCf8Am/IzAy+JZRwfMPgtffDnpDIlJRVvqNRfN++j3XtDBmz4bLLoP+/aFOnXDDleiozE1MPYEXzexeYA4wKBgfBDxnZsuBb4EOlQtRJM0Vu7Foy8/GvXvn8mB/44AD/LZ3f/xjuCFK9FQouTvnpgBTguOVwHY7KDrnfgL+XAWxiUhMkNjffx86dYKPPjKuvNIX8fvvH3JsEkm6Q1UkCXz/ve/MnH46bN4M48fDkCFK7FI6rS0jEnETJkCXLvDpp78u9LXPPmFHJVGnyl0kotavh2uugfPPh9/8Bt55B/79byV2KR8ld5EIeu01f/PR0KHQuzfMnetbMiLlpbaMSIR8+SXceCO8+io0bQpjx8IJJ4QdlSQjVe4iEeCcv0DasCGMGQP33QczZyqxy85T5S4Ssk8+ga5d/QyYU0/1m2kcc0zYUUmyU+UuEpLCQn9X6XHHwbvvwmOP+a9K7FIVVLmLhGDpUujc2a/aeP758NRTcPjhYUclqUSVu0g12rrVz1Nv0gQWLfLb3o0bp8QuVU+Vu0g1mTPHLx0wZw786U/w+OPw29+GHZWkKlXuIgn2009w++1w4onwxRd+muPw4Urskliq3EUSaNo031tfuhSuvhoeeQT22y/sqCQdqHIXSYDvvvM3I51xhq/cJ0zwG2kosUt1UXIXqWJvvumnNz7xhF/JceFCOPfcsKOSdKPkLlJFvvkGOnaECy6AmjV9S6ZfP9h777Ajk3Sk5C5SSc753ZAaNoQXXoB//csv9HXqqWFHJulMF1RFKuGLL6BbNxgxApo18731Jk3CjkpElbvITnEO/vMfX62PGwd9+sCMGUrsEh2q3EUqaNUqvzPSxIl+Nswzz8DRR4cdlUhRqtxFymnbNr+413HHwQcf+NkwU6YosUs0qXIXKYclS/zSAR98AG3awJNPwmGHhR2VSOlUuYvswNatfuOMpk39XabPPQdvvBEkdueKnlz8uUiIlNxFSjFrFmRm+qmNl17qq/e//Q3MgJwcyM7+NaE755/n5IQXsEgcJXeRYn78EXr2hJNPhnXr/DTHl16Cgw4KTnAONmyAvLxfE3x2tn++YYMqeIkE9dxF4kydCtdeC8uW+R77ww9DRkaxk8wgN9cf5+X5B/i1BnJzg9JeJFyq3EWATZvg+uvhrLOgoADeestPcdwuscfEJ/gYJXaJECV3SXtjx0KjRn6rux49YMECaN26jG+KtWLixffgRUKm5C5p6+uv/QXSCy+Efff10xxzc2Gvvcr4xvgee1aW3+k6K6toD14kZOq5S9pxzl8g7d7dX/+84w6/U9Iee5TzB5j5fk18jz3WosnIUGtGIsFcBKqMzMxMl5+fH3YYkgbWrIEbboBRo/w0x8GDoXHjnfxhzhVN5MWfiySYmc1yzmWW9JraMpIWnIOnn/YLfU2YAH37+jbMTid22D6RK7FLhKgtIylv5Uo/vfHtt6FlSz8L5qijwo5KJLFUuUvK2rbNt8KPOw7y8/16MG+/rcQu6aHM5G5mvzGzmWY2z8wWmdldwfgRZjbDzJab2UtmtnswvkfwfHnwev0E/xtEtrNwIZx2Gtx0k5/WuGgRXHcd7KJyRtJEeT7qW4BWzrkmQFPgAjNrAfQBcp1zRwHrgU7B+Z2A9cF4bnCeSLX4+We46y6/K9KKFX7bu1GjoF69sCMTqV5lJnfnfR883S14OKAV8EowPgS4NDhuFzwneL21ma40SRXZwUqMH34IzZv7tbv+/GdYvBguv1zXOSU9leuCqpntCswCjgL6AyuADc65guCU1UDd4Lgu8BmAc67AzDYCBwBfF/uZXYAuAIdpYWwpj5wcPzE9Nrc8uJlo8161uWPLP8nNhTp1YPRouOiisIMVCVe5krtzbhvQ1MwygBHAMZX9xc65gcBA8PPcK/vzJMXFr8QIPsFnZzM5bx7X1nqZFRt9T71PH6hVK9RIRSKhQlMhnXMbzGwycAqQYWY1guq9HrAmOG0NcCiw2sxqALWAb6owZklHxVZi3Jj3H26jLwPpx+8OdEx+3S/6JSJeeWbL1A4qdsxsT+BcYAkwGfhTcFpHYGRwPCp4TvD62y4Kt8FK8gsS/BgupBGLeIbO3HKzY/58U2IXKaY8s2XqAJPNbD7wITDROTcG6AncZGbL8T31QcH5g4ADgvGbgF5VH7ako3VrHVccM5uLGcP+fMt0WvBQQTY191TtIFJcmW0Z59x84IQSxlcCJ5Uw/hPw5yqJTgTfbn9xmOMfnX9g44+NuavFOHpNuYDde55WtAevaTEiv9DyAxJpq1f7TTTGjDFOrruBQWc/T6OhPbUSo0gZlNwlkgoL/Rowt94KW7fCo4/CP/5Rj1136flrIo8leCV2ke0ouUvkLF/uF/qaMgVatfKrOR55ZOxVrcQoUh5aaUMio6DAb0jduDHMmeMr97feik/sIlJeqtylepSxscWCBdCpk19CoF07eOIJOOSQEOIUSRGq3CXxcnKK7i0a24M0J4ctW+DOO/1CX6tW+e3vRoxQYhepLCV3Saz4ZQNiCT7YXHr6klo0a+a4+27o0AGWLIH27dVGF6kKastIYhVbNoC8PH6gJv9qOpm84S2pW9d44w1o2zbcMEVSjSp3Sby4BD+JVjRmAf3mnkXXrsaiRUrsIomg5C6J5xwbbridzjzNOUyiBgVM/dNjPNHfse++YQcnkprUlpHEco6Rlwzi+jHdWWsH0/NWx50/PM2e/R+G7JW6CUkkQZTcJWHWroXu3Y2Xx3Tm+APXMHrcLjTPNHB9ocZWLRsgkkBK7lLlnIPnn4cePeD77+Hee+G2Ww9ht921bIBIdVFylyr16afQtSuMGwennAKDBsGxx4KWDRCpXrqgKlWisNDfVdqoEUyd6mc9vvtuLLGLSHVT5S6V9vHH0LmzT+bnnAMDB8IRR4QdlUh6U+UuO62gwG9Iffzxfm2YwYNhwgQldpEoUOUuO2XePLjmGpg9Gy67DPr3hzp1wo5KRGJUuUuFbNkC//oXZGb6XZKGD4fXXlNiF4kaVe5Sbu+/75fl/egj6NjR7460//5hRyUiJVHlLmX6/nvIyoLTT4fNm2H8eHj2WSV2kShT5S47NGECdOni56936wb33w/77BN2VCJSFlXuUqL16+Hqq+H88+E3v/HTHB9/XIldJFkouct2RoyAhg3hueegd2+YOxdOOy3sqESkItSWkV98+SV07w6vvAInnABjx/qvIpJ8VLmnqth+paU9L/bSkCG+Wh89Gh54AGbMUGIXSWZK7qloBxtSF/fJJ9CmDVx1lV8XZt486NULdtutOgMWkaqm5J5qdrAhNRs2/JLwCwvh3//2Cf299/zx1Knw+9+HGr2IVBH13FNNCRtSA36ierCG+tKl/mak996D887zC30dfnh4IYtI1VPlnoriE3xMbi5bC4wHHoAmTWDxYt9nHz9eiV0kFSm5p6JYKybOnL8+zEknOW6/HS6+GJYsgSuv1J4ZIqlKyT3VxPfYs7L48YdCemdO5MRh2Xz58SZefcUxfDgcfHDYgYpIIim5pxozv/F0VhbT/phL0xOMB/PP4cqGs1h84wD+8EeV6iLpQBdUU9B3N+fQu5ej/5lG/fp+fZhzzzkJ7OSwQxORaqLKPcWMG+enNz4xwMjK8jsknXsuaq6LpJkyk7uZHWpmk81ssZktMrOsYHx/M5toZsuCr/sF42Zmj5nZcjObb2bNEv2PEPjmG3+BtG1b2HtvP82xXz9/LCLppzyVewFws3OuIdAC6GZmDYFewCTnXANgUvAcoA3QIHh0AQZUedTyC+f8bkgNG8KwYX6XpDlz4JRTwo5MRMJUZnJ3zn3hnJsdHH8HLAHqAu2AIcFpQ4BLg+N2wFDnTQcyzEybsCXA55/DH/4A7dvDoYdCfj7ccw/ssUfYkYlI2CrUczez+sAJwAzgYOfcF8FLXwKxyXV1gc/ivm11MFb8Z3Uxs3wzy1+3bl1F405rzsGgQb5aHz8e+vSB6dP9zUkiIlCB5G5mewOvAj2cc5viX3POOaD0ZQdL4Jwb6JzLdM5l1q5duyLfmtZWrvQXSDt39sl8/ny47TaooXlPIhKnXMndzHbDJ/b/OudeC4a/irVbgq9rg/E1wKFx314vGJNK2LbNXyBt3BhmzoQBA2DyZGjQIOzIRCSKyjNbxoBBwBLn3KNxL40COgbHHYGRceNXBrNmWgAb49o3shMWL4YzzvA3np51FixaBF27wi6ayCoipSjPH/OnAX8HFpjZ3GDsduBB4GUz6wR8ArQPXhsLtAWWA5uBq6sy4HSydavvp99zj9+79Pnn4YorNGVdRMpWZnJ3zk0DSksnrUs43wHdKhlX2svP98vyzp8PHTr4pWIOOijsqEQkWegP+4j58Ud/gfTkk+Hrr+H11/38dSV2EakIzbGIkHfe8dX68uVw7bXQt69fA0xEpKJUuUfApk1www3QsqXf/m7SJL87khK7iOwsJfeQjR3rF/p66im46SbfY2/VKuyoRCTZKbmH5Ouv4W9/gwsvhH33hfffh0cegb32CjsyEUkFSu7VzDl46SW/dMDLL8Mdd8Ds2f4CqohIVdEF1Wq0Zo3vrY8aBSee6NeHadw47KhEJBWpcq8GzsHTT/tqfeJE33754AMldhFJHFXuCbZihZ/WOHkynH22T/K/+13YUYlIqlPlniDbtsGjj/rqfNYsP7Vx0iQldhGpHqrcE2DhQn8z0syZcNFFfgXHevXCjkpE0okq9yr0889w113QrJlfd33YMH/xVIldRKqbKvcqMnOmr9YXLoTLL/cLfWkPEhEJiyr3Stq8GW65xW9IvX49jB4NL7ygxC4i4VLlXgmTJ/vt7lauhOuu82uv16oVdlQiIqrcd8rGjT6Zt2rld0OaPBmefFKJXUSiQ8m9gkaP9jcjPfOMb8fMm+e3vhMRiRIl93Jat85fKL3kEjjgAJgxAx56CGrWDDsyEZHtKbmXwTl/gfTYY+HVV+Huu/0WeJmZYUcmIlI6XVDdgc8+g+uvhzfe8Ks2Dhrk114XEYk6Ve4lKCz0F0gbNfIXSx99FN57T4ldRJKHKvdili3zC31NnQqtW/s1YY48MuyoREQqRpV7oKDAXyA9/niYO9ev3jhxohK7iCQnVe74fUs7dfIXStu1gyeegEMOCTsqEZGdl9aV+5Ytfpu75s3hk0/89ncjRiixi0jyS9vKffp0X60vXgx//zvk5vr56zgHWNjhiYhUStpV7j/8ANnZcOqp8N3qjYxt9xRDh7hfE3t2NuTkhB2miEilpFVynzTJ74zUrx9c39Wx8Ir7aTOyq0/oscSelwcbNgQVvIhIckqLtsyGDXDzzTB4MDRo4Kc5nnmmgXsQ9tjiE3penj85K8v3aEytGRFJXilfub/+ul/oa8gQ6NXLL/R15pnBi2Y+kcdTYheRFJCyyf2rr6B9e7jsMjjoIL/Q1wMPwJ57xp0Ua8XEi7VoRESSWMold+fgued8tT5yJNx3H3z4oZ/uuN2JsR57VpZfcyAryz9XgheRJJdSPfdPP/WbaIwf72fDPPOMX82xRGaQkVG0xx5r0WRkqDUjIknNXAQq1MzMTJefn7/T319YCAMG+J66c7790q2b3yWpTM4VTeTFn4uIRJSZzXLOlbgAeZnpz8wGm9laM1sYN7a/mU00s2XB1/2CcTOzx8xsuZnNN7NmVffPKNnSpdCyJdx4o9+keuFC6N69nIkdtk/kSuwikgLKkwKfBS4oNtYLmOScawBMCp4DtAEaBI8uwICqCbNkgwdDkyawaBE8+yy8+SbUr5/I3ygikhzKTO7OuXeAb4sNtwOGBMdDgEvjxoc6bzqQYWZ1qijW7Rx9NFx0kV9CoGNHFd0iIjE7e0H1YOfcF8Hxl8DBwXFd4LO481YHY19QjJl1wVf3HHbYYTsVxOmn+4eIiBRV6amQzl+RrfBVWefcQOdcpnMus3bt2pUNQ0RE4uxscv8q1m4Jvq4NxtcAh8adVy8YExGRarSzyX0U0DE47giMjBu/Mpg10wLYGNe+ERGRalJmz93MhgFnAQea2WrgTuBB4GUz6wR8ArQPTh8LtAWWA5uBqxMQs4iIlKHM5O6cu7yUl1qXcK4DulU2KBERqZyUW1tGRESU3EVEUpKSu4hICorEwmFmtg5/YTZMBwJfhxxDRSnmxEu2eEExV5coxHy4c67EG4UikdyjwMzyS1tdLaoUc+IlW7ygmKtL1GNWW0ZEJAUpuYuIpCAl918NDDuAnaCYEy/Z4gXFXF0iHbN67iIiKUiVu4hIClJyFxFJQWmb3M1slZktMLO5ZpYfjJW4N2zYzOz3QZyxxyYz62FmOWa2Jm68bchxRnq/3QrE/JCZfRTENcLMMoLx+mb2Y9z7/WSEYi71s2BmvYP3eamZnR+hmF+Ki3eVmc0NxkN/n83sUDObbGaLzWyRmWUF45H+PBfhnEvLB7AKOLDYWF+gV3DcC+gTdpwlxL0rfverw4Ec4JawY4qL7UygGbCwrPcUv3roOMCAFsCMCMV8HlAjOO4TF3P9+PMi9j6X+FkAGgLzgD2AI4AVwK5RiLnY648Ad0TlfQbqAM2C432Aj4P3MtKf5/hH2lbupShtb9goaQ2scM6FfUfvdlyE99stTUkxO+cmOOcKgqfT8ZvOREYp73Np2gEvOue2OOf+h1+O+6SEBVeKHcVsZoZfNnxYtQa1A865L5xzs4Pj74Al+C1DI/15jpfOyd0BE8xsVrCfK5S+N2yUdKDofwQ3Bn8GDo5KG6mYiu63GzXX4CuymCPMbI6ZTTWzM8IKqhQlfRaS4X0+A/jKObcsbiwy77OZ1QdOAGaQRJ/ndE7upzvnmgFtgG5mdmb8i87/rRWpeaJmtjtwCTA8GBoA/A5oit+E/JFwIiufKL6nO2Jm/wQKgP8GQ18AhznnTgBuAl4ws33Diq+YpPosFHM5RQuWyLzPZrY38CrQwzm3Kf61qH+e0za5O+fWBF/XAiPwf6qWtjdsVLQBZjvnvgJwzn3lnNvmnCsEniaEP7fLISn32zWzq4CLgL8G/xETtDa+CY5n4fvXR4cWZJwdfBai/j7XAP4AvBQbi8r7bGa74RP7f51zrwXDSfN5TsvkbmZ7mdk+sWP8BbSFlL43bFQUqXCK9fQuw/8boibp9ts1swuA24BLnHOb48Zrm9muwfGRQANgZThRFrWDz8IooIOZ7WFmR+Bjnlnd8e3AOcBHzrnVsYEovM/BdYBBwBLn3KNxLyXP5znsK7phPIAj8TMI5gGLgH8G4wcAk4BlwFvA/mHHGhfzXsA3QK24seeABcB8/IerTsgxDsP/Sb0V33PsVNp7ip9V0B9flS0AMiMU83J8/3Ru8HgyOPePwedlLjAbuDhCMZf6WQD+GbzPS4E2UYk5GH8W6Frs3NDfZ+B0fMtlftznoG3UP8/xDy0/ICKSgtKyLSMikuqU3EVEUpCSu4hIClJyFxFJQUruIiIpSMldRCQFKbmLiKSg/wcaNYlHBVhd6QAAAABJRU5ErkJggg==\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -268,7 +268,7 @@ { "data": { "image/svg+xml": [ - "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" ], "text/plain": [ "" @@ -515,7 +515,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -655,12 +655,12 @@ "output_type": "stream", "text": [ "\n", - "%0 = ConstantInput(1) # Integer\n", - "%1 = x_0 # Integer\n", - "%2 = ConstantInput(15) # Integer\n", + "%0 = ConstantInput(1) # Integer\n", + "%1 = x_0 # Integer\n", + "%2 = ConstantInput(15) # Integer\n", "%3 = Add(1, 2) # Integer\n", "%4 = Mul(3, 0) # Integer\n", - "%5 = ArbitraryFunction(4) # Integer\n", + "%5 = TLU(4) # Integer\n", "return(%5)\n" ] } @@ -711,7 +711,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -734,25 +734,7 @@ ] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 1b1ef297f..be6a880a8 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -94,7 +94,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -173,22 +173,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Epoch: 1 | Loss: 0.530019998550415\n", - "Epoch: 101 | Loss: 0.1248268187046051\n", - "Epoch: 201 | Loss: 0.07593712955713272\n", - "Epoch: 301 | Loss: 0.05418260768055916\n", - "Epoch: 401 | Loss: 0.04199932515621185\n", - "Epoch: 501 | Loss: 0.03424343094229698\n", - "Epoch: 601 | Loss: 0.028883913531899452\n", - "Epoch: 701 | Loss: 0.024963364005088806\n", - "Epoch: 801 | Loss: 0.021973103284835815\n", - "Epoch: 901 | Loss: 0.019618362188339233\n", - "Epoch: 1001 | Loss: 0.017716625705361366\n", - "Epoch: 1101 | Loss: 0.01614907570183277\n", - "Epoch: 1201 | Loss: 0.014835075475275517\n", - "Epoch: 1301 | Loss: 0.013717765919864178\n", - "Epoch: 1401 | Loss: 0.01275621633976698\n", - "Epoch: 1501 | Loss: 0.011920095421373844\n" + "Epoch: 1 | Loss: 0.5758869647979736\n", + "Epoch: 101 | Loss: 0.13611836731433868\n", + "Epoch: 201 | Loss: 0.08021673560142517\n", + "Epoch: 301 | Loss: 0.05636058747768402\n", + "Epoch: 401 | Loss: 0.043306026607751846\n", + "Epoch: 501 | Loss: 0.03511128947138786\n", + "Epoch: 601 | Loss: 0.029501130804419518\n", + "Epoch: 701 | Loss: 0.025424323976039886\n", + "Epoch: 801 | Loss: 0.02233024500310421\n", + "Epoch: 901 | Loss: 0.01990305446088314\n", + "Epoch: 1001 | Loss: 0.0179488193243742\n", + "Epoch: 1101 | Loss: 0.01634199731051922\n", + "Epoch: 1201 | Loss: 0.014997857622802258\n", + "Epoch: 1301 | Loss: 0.013856985606253147\n", + "Epoch: 1401 | Loss: 0.012876608408987522\n", + "Epoch: 1501 | Loss: 0.012025204487144947\n" ] } ], @@ -251,7 +251,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAT40lEQVR4nO3df2xdZ33H8c+XxluYkyURQcyuw7I/6KbDDeVHRjpRbVnQltJBqilMo9tgrYYibV06tElD449WG39NaAjPFURRqbLescIEFasrWIdy6aKV1VNoC27syaprg22sBRzfkPg2P6773R/nGhzX9r22j+9z7nPfL8nKvec88fn0afLJ8XPOvdfcXQCA1ve60AEAANmg0AEgEhQ6AESCQgeASFDoABCJLaEO3NnZ6bt27Qp1eOTMlStXtGPHDm3btk033XRT6DhAbj3//PM/cvc3LrcvWKHv2rVLx48fD3V45MzQ0JCOHDmi22+/Xdu3bw8dB8itzs7O7620jyUX5EKSJJqfn9fExISmp6dDxwFaEoWO3BgbG1OxWNTVq1dDRwFaEoUOAJGg0AEgEhQ6AESCQkfuXLt2LXQEoCVR6MiVcrmsarWq4eHh0FGAlkOhI1eSJFFfX1/oGEBLotABIBIUOgBEgkJHLrm7xsfHQ8cAWgqFjtxJkkS9vb2qVCq8DQCwBvEX+tLPTOUzVFtCoVBQqVQKHQNoKXXfbdHM9kh6VNKbJLmkk+7eu2SMSeqVdKekiqR73P257OOu0dNPS1euSIcPS2ZpmT/1lLR1q3TwYOh0QGYGB6VSSbp4UdqxQzp0SNq3L3SquOVxzhs5Q69K+it3TyTdJuk+M0uWjHmfpLfUvo5J+lymKdfDPS3zgYG0xBfKfGAg3c6ZOiIxOCj190vlcvrHulxOnw8Ohk4Wr7zOed0zdHefljRde3zJzIYl3SxpaNGwuyQ96u4u6Vkz22lmXbXfG4ZZemYupSU+MJA+PnDgp2fsQARKJen69Ru3Xb+ebg99xhirvM75mtbQzWyvpHdIGliy62ZJE4ueT9a2Lf39x8zsrJmdnZubW2PUdVhc6gso85YxNTWl2dlZ7nap4+LFtW3HxuV1zhsudDPbJukrkj7m7j9ez8Hc/aS773f3/Z2dnev5Fms9YLrMstjC8gtyr7u7W729vXrllVdCR8m1HTvWth0bl9c5b6jQzaxDaZl/wd0fX2bIlKQ9i5731LaFs3jN/MAB6YEH0l8Xr6kj9173uvhvxNqoQ4ekjo4bt3V0pNuxOfI6543c5WKSPi9p2N0/vcKwJyT9uZl9UdIBSReDrp9L6bLK1q03rpkvLL9s3cqyC6KxsGabtzsuYpbXOW/kQ6LfI+nDkgbN7IXatk9IerMkufsJSV9TesviS0pvW7w386TrcfBgeia+UN4LpU6ZIzL79oUvk3aTxzlv5C6X/5K0agPW7m65L6tQmVpa3pQ5gEg1coYOBJMkiUZHR+Xu2rVrl7q6ukJHAnKLK07IvbGxMd4GAGgAhQ4AkaDQASASFDoARIJCR8sol8uhIwC5RqGjJbi7RkdHNTIyEjoKkFsUOlpGf39/6AhArlHoABAJCh0AIkGho6VUq1XW0YEVUOhoGYVCQX19fRoZGdH0dNg38wTyiEJHS0mShE8wAlZAoQNAJCh0AIgEhQ4AkaDQ0XKmpqY0OzvL3S7AEhQ6Wk53d7d6e3tDxwByh0IHgEhQ6AAQCQodACJBoaNlVatVXbp0KXQMIDcodLSkQqGgUqmkiYkJSh2oodDRstxdL7/8cugYQG5Q6AAQCQodACJBoQNAJCh0tDwujAKpuoVuZo+Y2Xkze3GF/TvMrN/MvmNm58zs3uxjAssbGxtTqVTSzMxM6ChAcI2coZ+SdMcq+++TNOTut0o6KOkfzOxnNh4NaMzU1FToCEAu1C10dz8j6cJqQyRtNzOTtK02tppNPABAo7Zk8D0ekvSEpB9I2i7p99391eUGmtkxScckaefOnRkcGgCwIIuLooclvSCpW9LbJT1kZj+/3EB3P+nu+919f2dnZwaHBqQLFy6oUqloeHiYi6Noa1kU+r2SHvfUS5LGJP1KBt8XaEihUFBfXx8fHo22l0Whf1/SeyXJzN4k6Zcl8XpsAGiyumvoZvaY0rtXdpvZpKQHJXVIkrufkPRJSafMbFCSSfq4u/9o0xIDAJZVt9Dd/e46+38g6bczSwQAWBdeKYooJEmi+fl5Xb58OXQUIBgKHdF45plnNDs7y8VRtC0KHdHo7u5WsVgMHQMIhkIHgEhQ6AAQCQodACJBoSM6lUpF09PToWMATUehIyoLF0bL5XLoKEDTUeiITrlc5tZFtCUKHQAiQaEDQCQodACIBIWOKM3Pz2toaIi7XdBWKHREJ0kSjY2NqVgs6urVq6HjAE1DoQNAJCh0AIgEhY6oXbt2LXQEoGkodESrXC6rWq1qeHg4dBSgKSh0RCtJEvX19YWOATQNhQ4AkaDQASASFDoARIJCR/TcnXdfRFug0BG1JEnU29vLh16gLVDoiF6hUFCpVAodA9h0FDoARIJCB4BI1C10M3vEzM6b2YurjDloZi+Y2Tkz+89sIwIAGtHIGfopSXestNPMdkr6rKQj7v5WSb+XSTIgY7Ozs9ztgqjVLXR3PyPpwipD/kDS4+7+/dr48xllAzLj7urt7eXNuhC1LNbQb5G0y8yeNrNvm9lHVhpoZsfM7KyZnZ2bm8vg0ACABVsy+h7vkvReSa+X9N9m9qy7jywd6O4nJZ2UpJ6eHs/g2ACAmiwKfVLSjLvPSZozszOSbpX0mkIHAGyeLJZc/k3S7Wa2xcx+TtIBSbwBNXJpfn5ely5dCh0D2BR1z9DN7DFJByXtNrNJSQ9K6pAkdz/h7sNm9u+SvivpVUkPu/uKtzgCoRQKBY2OjsrdtWvXLnV1dYWOBGSqbqG7+90NjPmUpE9lkgjYRGNjYxofH9fRo0dDRwEyxytFASASFDoARIJCR1viwihilMVti0BLOXfunPbu3StJuuWWW8KGATLEGTraTpIk6u/vDx0DyByFDgCRoNABIBIUOgBEgkJH26pWqxoZ4S2HEA8KHW2pUCior69PIyMj3MKIaFDoaFtJkoSOAGSKQgeASFDoABAJCh1t7/Lly6EjAJmg0NHWnnnmGc3Ozmp8fDx0FGDDKHS0te7ubhWLxdAxgExQ6AAQCQodACJBoaPtXbhwQZVKhRcYoeVR6Gh7hUJBpVJJExMTlDpaGoUOSHJ3vfzyy6FjABtCoQNAJCh0AIgEhQ4AkaDQgUUmJye5MIqWRaEDNWNjYzp9+rRmZmZCRwHWhUIHFpmamgodAVi3uoVuZo+Y2Xkze7HOuF81s6qZfTC7eACARjVyhn5K0h2rDTCzmyT9vaT/yCATAGAd6ha6u5+RdKHOsOOSviLpfBahAABrt+E1dDO7WdLvSvpcA2OPmdlZMzs7Nze30UMDmVt4X5fh4eHQUYA1y+Ki6GckfdzdX6030N1Puvt+d9/f2dmZwaGBbBUKBfX19Wl8fJzbF9FytmTwPfZL+qKZSdJuSXeaWdXdv5rB9wYANGjDhe7uv7Tw2MxOSXqSMgeA5qtb6Gb2mKSDknab2aSkByV1SJK7n9jUdACAhtUtdHe/u9Fv5u73bCgNkBPz8/OamZnR9u3bQ0cBGsYrRYElkiRRf3+/KpWKxsfHQ8cBGkahA8soFAoqFouhYwBrQqEDQCQodACIBIUOrKJSqWh6ejp0DKAhFDqwgu7ubhWLRZXL5dBRgIZQ6MAqKHO0EgodACJBoQNAJCh0AIgEhQ7U4e4aGhribhfkHoUOrCJJEp0+fVqlUil0FKAuCh0AIkGhA0AkKHQAiASFDjSIzxhF3lHoQAPOnTunarWqkZGR0FGAFVHoQAOSJFFvb2/oGMCqKHQAiASFDgCRoNABIBIUOrAG1WqVD45GblHoQIMKhYJ6e3v5FCPkFoUOrEGhUOB9XZBbFDoARIJCB4BIUOjAOszOznJxFLlTt9DN7BEzO29mL66w/w/N7LtmNmhm3zKzW7OPCeSHu6u3t1fXrl0LHQW4QSNn6Kck3bHK/jFJv+Hu+yR9UtLJDHIBANZoS70B7n7GzPausv9bi54+K6kng1wAgDXKeg39TyR9faWdZnbMzM6a2dm5ubmMDw0A7a3uGXqjzOw3lRb67SuNcfeTqi3J9PT0eFbHBkKYn58PHQG4QSaFbmZvk/SwpPe5+0wW3xPIs0KhoNHRUbm79uzZo+3bt4eOBGx8ycXM3izpcUkfdnfe/R9tY2xsjFeNIlfqnqGb2WOSDkrabWaTkh6U1CFJ7n5C0gOS3iDps2YmSVV3379ZgQEAy2vkLpe76+z/qKSPZpYIALAuvFIUACJBoQMbNDk5GToCIIlCBzbE3TU6OqqREe4HQHgUOrBB/f39oSMAkih0AIgGhQ4AkaDQASASFDqQgWq1yoVRBEehAxtUKBTU19enkZERXbp0KXQctDEKHchAkiShIwAUOgDEgkIHgEhQ6EBGxsfHNTExofHx8dBR0KYodCAj7q5isRg6BtoYhQ4AkaDQASASFDoARIJCBzJWqVR4gRGCoNCBDHV3d6tUKmlycpJSR9NR6EDGzp07x62LCIJCB4BIUOgAEAkKHQAiQaEDm2B+fp4Lo2g6Ch3IWJIkGhsb0+nTpzUzMxM6DtoIhQ5skqmpqdAR0GYodACIRPyF7r76c2SPOQeC2FJvgJk9Iun9ks67e2GZ/SapV9KdkiqS7nH357IOui5PPy1duSIdPiyZpcXy1FPS1q3SwYOh08WJOUebGByUSiXp4kVpxw7p0CFp376wmRo5Qz8l6Y5V9r9P0ltqX8ckfW7jsTLgnhbLwEBaKAvFMjCQbuesMXvM+Q0uXLigSqWi4eHh0FGQscFBqb9fKpfTP9blcvp8cDBsrrpn6O5+xsz2rjLkLkmPurtLetbMdppZl7tPZxVyXczSs0QpLZSBgfTxgQM/PXtEtpjzGxQKBfX19en+++8PHQUZK5Wk69dv3Hb9ero95Fl6FmvoN0uaWPR8srbtNczsmJmdNbOzc3NzGRy6jsUFs6ANi6WpmHO0gYsX17a9WZp6UdTdT7r7fnff39nZ2YwDpj/yL7awFIDNwZyjDezYsbbtzZJFoU9J2rPoeU9tW1iL128PHJAeeCD9dfH6LrLFnC/L3TU9HXYFEtk6dEjq6LhxW0dHuj2kLAr9CUkfsdRtki4GXz+X0h/xt269cf328OH0+datLAFsBub8NZIkUbFY1OzsLKUekX37pA98QNq5M/1jvXNn+jz0XS7mdc6azOwxSQcl7Zb0f5IelNQhSe5+onbb4kNK74SpSLrX3c/WO3BPT48fP358Q+Eb4n5jkSx9juwx569hZjp69Ki6urpCR0GL6+zs/La7719uXyN3udxdZ79Lum+d2Tbf0iJp82JpCuYcCCL+V4oCQJug0AEgEhQ60CRcGMVmo9CBJnB3FYtFPvACm4pCB5qkXC6HjoDIUegAEAkKHQAiQaEDQCQodKCJqtWqhoaGuNsFm4JCB5okSRKdPn1apVIpdBREikIHgEhQ6AAQibrvtrhpBzb7oaTvNfGQuyX9qInHy1KrZm/V3FLrZm/V3FLrZm927l909zcutyNYoTebmZ1d6S0n865Vs7dqbql1s7dqbql1s+cpN0suABAJCh0AItFOhX4ydIANaNXsrZpbat3srZpbat3sucndNmvoABC7djpDB4CoUegAEImoCt3MHjGz82b24gr7zcz+0cxeMrPvmtk7m51xJQ1kP2hmF83shdrXA83OuBwz22Nm3zSzITM7Z2Z/scyY3M17g7nzOudbzex/zOw7tex/u8yYnzWzL9XmfMDM9gaIujRTI7nvMbMfLprzj4bIuhIzu8nMnjezJ5fZF37O3T2aL0m/Lumdkl5cYf+dkr4uySTdJmkgdOY1ZD8o6cnQOZfJ1SXpnbXH2yWNSEryPu8N5s7rnJukbbXHHZIGJN22ZMyfSTpRe/whSV9qkdz3SHoodNZV/hv+UtK/LPfnIg9zHtUZurufkXRhlSF3SXrUU89K2mlmXc1Jt7oGsueSu0+7+3O1x5ckDUu6ecmw3M17g7lzqTaPl2tPO2pfS+9uuEvSP9Uef1nSe83MmhRxWQ3mzi0z65H0O5IeXmFI8DmPqtAbcLOkiUXPJ9Uif4lrfq324+rXzeytocMsVfsR8x1Kz7wWy/W8r5Jbyumc1370f0HSeUnfcPcV59zdq5IuSnpDU0Muo4HcknS0tjT3ZTPb09yEq/qMpL+W9OoK+4PPebsVeit7Tul7ONwqqU/SV8PGuZGZbZP0FUkfc/cfh87TqDq5czvn7j7v7m+X1CPp3WZWCBypIQ3k7pe0193fJukb+ukZb1Bm9n5J593926GzrKbdCn1K0uJ/8Xtq23LP3X+88OOqu39NUoeZ7Q4cS5JkZh1KS/EL7v74MkNyOe/1cud5zhe4e1nSNyXdsWTXT+bczLZI2iFppqnhVrFSbnefcfertacPS3pXk6Ot5D2SjpjZuKQvSjpkZv+8ZEzwOW+3Qn9C0kdqd13cJumiu7fER8eY2S8srMeZ2buV/r8L/he0lunzkobd/dMrDMvdvDeSO8dz/kYz21l7/HpJvyXpf5cMe0LSH9cef1BSyWtX60JpJPeSaytHlF7bCM7d/8bde9x9r9ILniV3/6Mlw4LP+ZZmHmyzmdljSu9M2G1mk5IeVHrhRe5+QtLXlN5x8ZKkiqR7wyR9rQayf1DSn5pZVdIrkj4U+i9ozXskfVjSYG1tVJI+IenNUq7nvZHceZ3zLkn/ZGY3Kf1H5l/d/Ukz+ztJZ939CaX/WBXN7CWlF9s/FC7uTzSS+34zOyKpqjT3PcHSNiBvc85L/wEgEu225AIA0aLQASASFDoARIJCB4BIUOgAEAkKHQAiQaEDQCT+H+Ww0z5fuBGwAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] @@ -289,9 +289,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.54424667]\n", - " [2.37960148]]\n", - "-14.69552993774414\n" + "[[4.53586054]\n", + " [2.37015319]]\n", + "-14.660321235656738\n" ] } ], @@ -330,7 +330,7 @@ { "data": { "image/svg+xml": [ - "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" ], "text/plain": [ "" @@ -601,7 +601,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -762,18 +762,18 @@ "output_type": "stream", "text": [ "\n", - "%0 = ConstantInput(2) # Integer\n", - "%1 = ConstantInput(1) # Integer\n", - "%2 = x_0 # Integer\n", - "%3 = ConstantInput(6) # Integer\n", - "%4 = x_1 # Integer\n", - "%5 = ConstantInput(6) # Integer\n", - "%6 = Add(2, 3) # Integer\n", - "%7 = Add(4, 5) # Integer\n", + "%0 = ConstantInput(2) # Integer\n", + "%1 = ConstantInput(1) # Integer\n", + "%2 = x_0 # Integer\n", + "%3 = ConstantInput(6) # Integer\n", + "%4 = x_1 # Integer\n", + "%5 = ConstantInput(6) # Integer\n", + "%6 = Add(2, 3) # Integer\n", + "%7 = Add(4, 5) # Integer\n", "%8 = Mul(6, 0) # Integer\n", - "%9 = Mul(7, 1) # Integer\n", + "%9 = Mul(7, 1) # Integer\n", "%10 = Add(8, 9) # Integer\n", - "%11 = ArbitraryFunction(10) # Integer\n", + "%11 = TLU(10) # Integer\n", "return(%11)\n" ] } @@ -824,7 +824,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAQaElEQVR4nO3df2xdZ33H8fd3jTtTx0siUrGkDssqkY2QUKAZ6QTavERbf6FW05hGt8FaDaXaSje0SkPjj1Ybf01oiK4IoqhUoRtrmaBibVXWoYQuGqyeQltwSqaqagOERgq0iuncdkrW7/4419Qxtu+xc67P9eP3S7rqvec8vueTp/Ynx889NzcyE0nS8vczbQeQJDXDQpekQljoklQIC12SCmGhS1IhVrV14KGhoVy3bl1bh1ehTp8+zYUXXsj555/fdhSpJx5//PEfZeaFs+1rrdDXrVvHzTff3NbhVajnnnuOG2+8kc2bN7cdReqJoaGh7861zyUXSSqEhS5JhbDQJakQFrokFcJCl6RCWOiSVAgLXZIKYaFLUiEsdEkqhIUuSYWw0CWpEBa6JBWi/EKf+ZmpfoaqpEJ1/dcWI2ITcDfwBiCBfZl5+4wxAdwOXAW8BFyfmY81H3eBHnkEXnkFLr8cIqoyf/hhGByE0dG200mNGR+HgwdhYgLWrIFdu2D79rZTla0f57zOGfoZ4JbM3ApcBtwUEVtnjLkSeFPntgf4TKMpFyOzKvOxsarEp8p8bKza7pm6CjE+Dg88AKdOVd/Wp05Vj8fH205Wrn6d865n6Jl5AjjRuf9iRBwFLgK+M23YtcDdmZnAoxGxNiI2dL62HRHVmTlUJT42Vt3fufO1M3apAAcPwunTZ287fbra3vYZY6n6dc4XtIYeEZuBtwNjM3ZdBHx/2uPjnW0zv35PRByOiMOTk5MLjLoI00t9imWuwkxMLGy7zl2/znntQo+I1cCXgA9n5o8Xc7DM3JeZOzJzx9DQ0GKeYqEHrJZZpptafpEKsWbNwrbr3PXrnNcq9IgYoCrzz2fmfbMM+QGwadrjkc629kxfM9+5E269tfrv9DV1qQC7dsHAwNnbBgaq7eqNfp3zOle5BPBZ4GhmfmKOYfcDH4qIe4GdwESr6+dQLasMDp69Zj61/DI46LKLijG1ZttvV1yUrF/nvM6HRL8LeD8wHhFPdLZ9FHgjQGbuBR6iumTxaarLFm9oPOlijI5WZ+JT5T1V6pa5CrN9e/tlstL045zXucrlP4B5G7BzdctNTYVq1MzytswlFar8d4pK0gphoUtSIeqsoUvLyssvv8yJE/Vek1+9ejXDw8M9TiQtDQtdRdm4cSMHDhyoPX7Xrl1s2rTJUlcRXHJRcTKz9u2ZZ55pO67UGAtdkgphoUtSISx0SSqEhS5JhbDQJakQFrokFcJCl6RCWOiSVAgLXZIKYaFLUiEsdEkqhIUuSYWw0CWpEBa6JBXCQpekQljoklQIC12SCmGhS1IhLHRJKoSFLkmFsNAlqRAWuiQVwkKXpEJ0LfSIuCsiTkbEkTn2r4mIByLiWxHxZETc0HxMSVI3dc7Q9wNXzLP/JuA7mXkJMAr8XUScf+7RJEkL0bXQM/MQ8MJ8Q4DhiAhgdWfsmWbiSZLqWtXAc3wKuB94DhgGfi8zX51tYETsAfYArF27toFDS5KmNPGi6OXAE8BG4G3ApyLi52YbmJn7MnNHZu4YGhpq4NCSpClNFPoNwH1ZeRp4FvjlBp5XkrQATRT694DdABHxBuCXgGcaeF5J0gJ0XUOPiHuorl5ZHxHHgduAAYDM3At8DNgfEeNAAB/JzB/1LLEkaVZdCz0zr+uy/zngtxpLJElaFN8pKkmFsNAlqRAWuiQVwkKXpEJY6JJUCAtdkgphoUtSISx0SSqEhS5JhbDQJakQFrokFcJCl6RCNPGJRdKydezYMS6++OLa41etWsWWLVt6mEhaPAtdK1pmcuDAgVpjjxw5wi233NLjRNLiueQiSYWw0CWpEBa6JBXCQpekQljoklQIC12SCmGhS1IhLHRJKoSFLkmFsNAlqRAWuiQVwkKXpEJY6JJUiK6FHhF3RcTJiDgyz5jRiHgiIp6MiH9vNqIkqY46Z+j7gSvm2hkRa4FPA9dk5luA320kmSRpQboWemYeAl6YZ8jvA/dl5vc64082lE2StABNrKFvAdZFxCMR8c2I+MBcAyNiT0QcjojDk5OTDRxakjSliU8sWgVcCuwGXgf8Z0Q8mplPzRyYmfuAfQAjIyPZwLElSR1NFPpx4PnMnAQmI+IQcAnwU4UuSeqdJpZc/gV4d0SsiogLgJ3A0QaeV5K0AF3P0CPiHmAUWB8Rx4HbgAGAzNybmUcj4l+BbwOvAndm5pyXOEqSeqNroWfmdTXGfBz4eCOJJEmL4jtFJakQFrokFcJCl6RCWOiSVAgLXZIKYaFLUiEsdEkqhIUuSYWw0CWpEBa6JBXCQpekQljoklQIC12SCmGhS1IhLHRJKoSFLkmFsNAlqRAWuiQVwkKXpEJY6JJUCAtdkgqxqu0A0nJy5swZnnrqqQV9zYYNGxgeHu5RIuk1FrpU07Zt27j99tsX9DXbt29n9+7dvPnNb+5RKuk1Frq0ANu2bVvQ+CeffJLdu3f3KI10NtfQJakQFrokFcJCl6RCdC30iLgrIk5GxJEu434lIs5ExHubiydJqqvOGfp+4Ir5BkTEecDfAv/WQCZJ0iJ0LfTMPAS80GXYzcCXgJNNhJIkLdw5r6FHxEXAbwOfqTF2T0QcjojDk5OT53poSdI0Tbwo+kngI5n5areBmbkvM3dk5o6hoaEGDi1JmtLEG4t2APdGBMB64KqIOJOZX27guSVJNZ1zoWfmL07dj4j9wIOWuSQtva6FHhH3AKPA+og4DtwGDABk5t6eppMk1da10DPzurpPlpnXn1MaSdKi+U5RSSqEhS5JhbDQJakQFrokFcJCl6RCWOiSVAgLXZIKYaFLUiEsdEkqhIUuSYWw0CWpEBa6JBXCQpekQljoklQIC12SCmGhS1IhLHRJKoSFLkmFsNAlqRAWuiQVwkKXpEJY6JJUCAtdkgphoUtSISx0SSqEhS5JhbDQJakQXQs9Iu6KiJMRcWSO/X8QEd+OiPGI+EZEXNJ8TElSN3XO0PcDV8yz/1ng1zNzO/AxYF8DuSRJC7Sq24DMPBQRm+fZ/41pDx8FRhrIJUlaoK6FvkB/DHxlrp0RsQfYA7B27dqGDy31rxdffLH22OHh4R4mUckaK/SI+A2qQn/3XGMycx+dJZmRkZFs6thSv9q6dSsHDhxg8+bNtcZffPHFXHDBBbXHS9M1UugR8VbgTuDKzHy+ieeUSpGZPPvss7XGfv3rX+fGG2/scSKV6pwvW4yINwL3Ae/PzKfOPZIkaTG6nqFHxD3AKLA+Io4DtwEDAJm5F7gVeD3w6YgAOJOZO3oVWJI0uzpXuVzXZf8HgQ82lkiStCi+U1SSCmGhS1IhLHRJKoSFLkmFsNAlqRAWuiQVwkKXpEJY6JJUCAtdkgphoUtSISx0SSqEhS5JhbDQJakQFrokFcJCl6RCWOiSVAgLXZIKYaFLUiEsdEkqhIUuSYWw0CWpEBa6JBXCQpekQljoklQIC12SCmGhS1IhLHRJKkT5hZ45/2M1zzmXWrGq24CIuAt4D3AyM7fNsj+A24GrgJeA6zPzsaaDLsojj8Arr8Dll0NEVSwPPwyDgzA62na6MjnnWiHGx+HgQZiYgDVrYNcu2L693Ux1ztD3A1fMs/9K4E2d2x7gM+ceqwGZVbGMjVWFMlUsY2PVds8am+eca4UYH4cHHoBTp6pv61Onqsfj4+3m6nqGnpmHImLzPEOuBe7OzAQejYi1EbEhM080FXJRIqqzRKgKZWysur9z52tnj2qWc64V4uBBOH367G2nT1fb2zxLb2IN/SLg+9MeH+9s+ykRsSciDkfE4cnJyQYO3cX0gplisfSWc64VYGJiYduXypK+KJqZ+zJzR2buGBoaWooDVr/yTze1FKDecM61AqxZs7DtS6XrkksNPwA2TXs80tnWrunrt1O/8k89Bs8ae8E51wqxa1e1Zj592WVgoNrepiYK/X7gQxFxL7ATmGh9/Ryq4hgcPHv9dmopYHDQYukF51wrxNQ6eb9d5VLnssV7gFFgfUQcB24DBgAycy/wENUli09TXbZ4Q6/CLtjoaHXWOFUkUwVjsfSOc64VYvv29gt8pjpXuVzXZX8CNzWWqGkzi8Ri6T3nXGpF+e8UlaQVwkKXpEJY6JJUiCaucpHUoJdeeomjR4/WGnveeeexZcuWHifScmGhS31k48aN3HHHHbXHX3311QwPD7Nhw4YeptJyYaFLfWbr1q21xx47doxLL720h2m0nLiGLkmFsNAlqRAWuiQVwkKXpEJY6JJUCAtdkgoR2dIHD0TED4HvLuEh1wM/WsLjNWm5Zl+uuWH5Zl+uuWH5Zl/q3L+QmRfOtqO1Ql9qEXE4M3e0nWMxlmv25Zoblm/25Zoblm/2fsrtkoskFcJCl6RCrKRC39d2gHOwXLMv19ywfLMv19ywfLP3Te4Vs4YuSaVbSWfoklQ0C12SClFUoUfEXRFxMiKOzLE/IuLvI+LpiPh2RLxjqTPOpUb20YiYiIgnOrdblzrjbCJiU0R8LSK+ExFPRsSfzzKm7+a9Zu5+nfPBiPiviPhWJ/tfzzLmZyPiC505H4uIzS1EnZmpTu7rI+KH0+b8g21knUtEnBcRj0fEg7Psa3/OM7OYG/BrwDuAI3Psvwr4ChDAZcBY25kXkH0UeLDtnLPk2gC8o3N/GHgK2Nrv814zd7/OeQCrO/cHgDHgshlj/hTY27n/PuALyyT39cCn2s46z5/hL4B/mu37oh/mvKgz9Mw8BLwwz5Brgbuz8iiwNiL64qNeamTvS5l5IjMf69x/ETgKXDRjWN/Ne83cfakzj//TeTjQuc28uuFa4HOd+18EdkdELFHEWdXM3bciYgS4GrhzjiGtz3lRhV7DRcD3pz0+zjL5Ie741c6vq1+JiLe0HWamzq+Yb6c685qur+d9ntzQp3Pe+dX/CeAk8NXMnHPOM/MMMAG8fklDzqJGboDf6SzNfTEiNi1twnl9EvhL4NU59rc+5yut0Jezx6j+DYdLgDuAL7cb52wRsRr4EvDhzPxx23nq6pK7b+c8M/8vM98GjADvjIhtLUeqpUbuB4DNmflW4Ku8dsbbqoh4D3AyM7/Zdpb5rLRC/wEw/W/8kc62vpeZP576dTUzHwIGImJ9y7EAiIgBqlL8fGbeN8uQvpz3brn7ec6nZOYp4GvAFTN2/WTOI2IVsAZ4fknDzWOu3Jn5fGb+b+fhnUC/fGDqu4BrIuIYcC+wKyL+ccaY1ud8pRX6/cAHOlddXAZMZOaJtkPVERE/P7UeFxHvpPp/1/oPaCfTZ4GjmfmJOYb13bzXyd3Hc35hRKzt3H8d8JvAf88Ydj/wR5377wUOZufVurbUyT3jtZVrqF7baF1m/lVmjmTmZqoXPA9m5h/OGNb6nK9ayoP1WkTcQ3VlwvqIOA7cRvXCC5m5F3iI6oqLp4GXgBvaSfrTamR/L/AnEXEGeBl4X9s/oB3vAt4PjHfWRgE+CrwR+nre6+Tu1znfAHwuIs6j+kvmnzPzwYj4G+BwZt5P9ZfVP0TE01Qvtr+vvbg/USf3n0XENcAZqtzXt5a2hn6bc9/6L0mFWGlLLpJULAtdkgphoUtSISx0SSqEhS5JhbDQJakQFrokFeL/Admi1qH7N00UAAAAAElFTkSuQmCC\n", "text/plain": [ "
" ] @@ -856,25 +856,7 @@ ] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index baed5e957..63f9f665d 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Optional import networkx as nx -from ..debugging.draw_graph import get_printable_graph +from ..debugging import draw_graph, get_printable_graph from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -70,6 +70,13 @@ class CompilationArtifacts: with open(output_directory.joinpath("graph.txt"), "w") as f: f.write(f"{get_printable_graph(self.operation_graph, show_data_types=True)[1:]}\n") + draw_graph( + self.operation_graph, + save_to=output_directory.joinpath("graph.png"), + block_until_user_closes_graph=False, + draw_edge_numbers=True, + ) + if self.bounds is not None: with open(output_directory.joinpath("bounds.txt"), "w") as f: # TODO: diff --git a/hdk/common/debugging/__init__.py b/hdk/common/debugging/__init__.py index 31dbfc844..a5253afdc 100644 --- a/hdk/common/debugging/__init__.py +++ b/hdk/common/debugging/__init__.py @@ -1,2 +1,3 @@ """Module for debugging.""" -from .draw_graph import draw_graph, get_printable_graph +from .drawing import draw_graph +from .printing import get_printable_graph diff --git a/hdk/common/debugging/draw_graph.py b/hdk/common/debugging/drawing.py similarity index 71% rename from hdk/common/debugging/draw_graph.py rename to hdk/common/debugging/drawing.py index fc63401b4..d430b99f5 100644 --- a/hdk/common/debugging/draw_graph.py +++ b/hdk/common/debugging/drawing.py @@ -1,5 +1,7 @@ """functions to draw the different graphs we can generate in the package, eg to debug.""" -from typing import Any, Dict, List + +from pathlib import Path +from typing import Dict, List, Optional import matplotlib.pyplot as plt import networkx as nx @@ -86,21 +88,40 @@ def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float return pos +def adjust_limits(): + """Increases the limits of x and y axis of the current pyplot figure by 20%. + + Returns: + None + """ + + x_lim = plt.xlim() + x_distance = x_lim[1] - x_lim[0] + plt.xlim([x_lim[0] - x_distance / 10, x_lim[1] + x_distance / 10]) + + y_lim = plt.ylim() + y_distance = y_lim[1] - y_lim[0] + plt.ylim([y_lim[0] - y_distance / 10, y_lim[1] + y_distance / 10]) + + def draw_graph( opgraph: OPGraph, block_until_user_closes_graph: bool = True, draw_edge_numbers: bool = True, + save_to: Optional[Path] = None, ) -> None: """Draw a graph. Args: - graph (OPGraph): The graph that we want to draw + opgraph (OPGraph): The graph that we want to draw block_until_user_closes_graph (bool): if True, will wait the user to close the figure before continuing; False is useful for the CI tests draw_edge_numbers (bool): if True, add the edge number on the arrow linking nodes, eg to differentiate the x and y in a Sub coding (x - y). This option is not that useful for commutative ops, and may make the picture a bit too dense, so could be deactivated + save_to (Optional[Path]): if specified, the drawn graph will be saved + to this path Returns: None @@ -211,89 +232,12 @@ def draw_graph( plt.axis("off") + adjust_limits() + + # save the figure if requested + if save_to is not None: + plt.savefig(save_to) + # block_until_user_closes_graph is used as True for real users and False # for CI plt.show(block=block_until_user_closes_graph) - - -def output_data_type_to_string(node): - """Return the datatypes of the outputs of the node. - - Args: - node: a graph node - - Returns: - str: a string representing the datatypes of the outputs of the node - - """ - return ", ".join([str(o.data_type) for o in node.outputs]) - - -def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: - """Return a string representing a graph. - - Args: - graph (OPGraph): The graph that we want to draw - show_data_types (bool): Whether or not showing data_types of nodes, eg - to see their width - - Returns: - str: a string to print or save in a file - """ - assert isinstance(opgraph, OPGraph) - list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) - graph = opgraph.graph - - returned_str = "" - - i = 0 - map_table: Dict[Any, int] = {} - - for node in nx.topological_sort(graph): - - if isinstance(node, ir.Input): - what_to_print = node.input_name - elif isinstance(node, ir.ConstantInput): - what_to_print = f"ConstantInput({node.constant_data})" - else: - - base_name = node.__class__.__name__ - - if isinstance(node, ir.ArbitraryFunction): - base_name = node.op_name - - what_to_print = base_name + "(" - - # Find all the names of the current predecessors of the node - list_of_arg_name = [] - - for pred, index_list in graph.pred[node].items(): - for index in index_list.values(): - # Remark that we keep the index of the predecessor and its - # name, to print sources in the right order, which is - # important for eg non commutative operations - list_of_arg_name += [(index["input_idx"], str(map_table[pred]))] - - # Some checks, because the previous algorithm is not clear - assert len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name)) - list_of_arg_name.sort() - assert [x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name))) - - # Then, just print the predecessors in the right order - what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" - - new_line = f"%{i} = {what_to_print}" - - # Manage datatypes - if show_data_types: - new_line = f"{new_line: <40s} # {output_data_type_to_string(node)}" - - returned_str += f"\n{new_line}" - - map_table[node] = i - i += 1 - - return_part = ", ".join(["%" + str(map_table[n]) for n in list_of_nodes_which_are_outputs]) - returned_str += f"\nreturn({return_part})" - - return returned_str diff --git a/hdk/common/debugging/printing.py b/hdk/common/debugging/printing.py new file mode 100644 index 000000000..713a3a3b8 --- /dev/null +++ b/hdk/common/debugging/printing.py @@ -0,0 +1,91 @@ +"""functions to print the different graphs we can generate in the package, eg to debug.""" + +from typing import Any, Dict + +import networkx as nx + +from ..operator_graph import OPGraph +from ..representation import intermediate as ir + + +def output_data_type_to_string(node): + """Return the datatypes of the outputs of the node. + + Args: + node: a graph node + + Returns: + str: a string representing the datatypes of the outputs of the node + + """ + return ", ".join([str(o.data_type) for o in node.outputs]) + + +def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: + """Return a string representing a graph. + + Args: + opgraph (OPGraph): The graph that we want to draw + show_data_types (bool): Whether or not showing data_types of nodes, eg + to see their width + + Returns: + str: a string to print or save in a file + """ + assert isinstance(opgraph, OPGraph) + list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) + graph = opgraph.graph + + returned_str = "" + + i = 0 + map_table: Dict[Any, int] = {} + + for node in nx.topological_sort(graph): + + if isinstance(node, ir.Input): + what_to_print = node.input_name + elif isinstance(node, ir.ConstantInput): + what_to_print = f"ConstantInput({node.constant_data})" + else: + + base_name = node.__class__.__name__ + + if isinstance(node, ir.ArbitraryFunction): + base_name = node.op_name + + what_to_print = base_name + "(" + + # Find all the names of the current predecessors of the node + list_of_arg_name = [] + + for pred, index_list in graph.pred[node].items(): + for index in index_list.values(): + # Remark that we keep the index of the predecessor and its + # name, to print sources in the right order, which is + # important for eg non commutative operations + list_of_arg_name += [(index["input_idx"], str(map_table[pred]))] + + # Some checks, because the previous algorithm is not clear + assert len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name)) + list_of_arg_name.sort() + assert [x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name))) + + # Then, just print the predecessors in the right order + what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" + + new_line = f"%{i} = {what_to_print}" + + # Manage datatypes + if show_data_types: + new_line = f"{new_line: <40s} # {output_data_type_to_string(node)}" + + returned_str += f"\n{new_line}" + + map_table[node] = i + i += 1 + + return_part = ", ".join(["%" + str(map_table[n]) for n in list_of_nodes_which_are_outputs]) + returned_str += f"\nreturn({return_part})" + + return returned_str diff --git a/script/nbmake_utils/notebook_sanitize.py b/script/nbmake_utils/notebook_sanitize.py index d1579f2a4..b553f8d52 100644 --- a/script/nbmake_utils/notebook_sanitize.py +++ b/script/nbmake_utils/notebook_sanitize.py @@ -15,6 +15,7 @@ def main(): with open(notebook_file, "w", newline="\n") as f: json.dump(notebook_dict, f, indent=1, ensure_ascii=False) + f.write("\n") if __name__ == "__main__": diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 0e2c75697..d36918afd 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -30,6 +30,7 @@ def test_artifacts_export(): assert output_directory.joinpath("environment.txt").exists() assert output_directory.joinpath("requirements.txt").exists() assert output_directory.joinpath("graph.txt").exists() + assert output_directory.joinpath("graph.png").exists() assert output_directory.joinpath("bounds.txt").exists() # format of those files might change in the future From 6959fdf7e7d65f569d568a5ad2e791cb8eee57ff Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 10:45:17 +0200 Subject: [PATCH 0109/1104] chore(tools): only make setup_env in dev docker if venv does not exist --- docker/Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index af09e3aa0..71a27ddfd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,9 +3,12 @@ FROM ghcr.io/zama-ai/zamalang-compiler RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-is-python3 git && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry && \ - echo "python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ - echo "cd /hdk/ && make setup_env" >> /root/.bashrc && \ + echo "if [[ \"\$?\" != \"0\" ]]; then" >> /root/.bashrc && \ + echo " python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ + echo " source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ + echo " cd /hdk/ && make setup_env" >> /root/.bashrc && \ + echo "fi" >> /root/.bashrc && \ echo "export LD_PRELOAD=/concrete/target/release/libconcrete_ffi.so" >> /root/.bashrc WORKDIR /hdk From f91092ac38a0daec6773a16b907e69720ab56cd8 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 17:39:59 +0200 Subject: [PATCH 0110/1104] tools: update pytest make target to have a report on coverage - allows to have coverage information without using make coverage - make coverage remains recommended to check a commit's content --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d4b43eb19..eca4d4c49 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ pcc_internal: check_python_format python_linting mypy_ci pydocstyle .PHONY: pcc_internal pytest: - poetry run pytest -svv --cov=hdk --cov-report=xml tests/ + poetry run pytest -svv --cov=hdk --cov-report=term-missing:skip-covered --cov-report=xml tests/ .PHONY: pytest # Not a huge fan of ignoring missing imports, but some packages do not have typing stubs From 371ecae80155ad4879ad9ff7c4d00de1edc06070 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 16 Aug 2021 19:06:04 +0200 Subject: [PATCH 0111/1104] tests: do not test BaseValue __repr__ anymore --- hdk/common/data_types/values.py | 2 +- tests/common/data_types/test_values.py | 26 -------------------------- 2 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 tests/common/data_types/test_values.py diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index 21c6de931..eda60eeb5 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -13,7 +13,7 @@ class BaseValue(ABC): def __init__(self, data_type: base.BaseDataType) -> None: self.data_type = data_type - def __repr__(self) -> str: + def __repr__(self) -> str: # pragma: no cover return f"{self.__class__.__name__}<{self.data_type!r}>" def __eq__(self, other: object) -> bool: diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py deleted file mode 100644 index 07dbf7462..000000000 --- a/tests/common/data_types/test_values.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Test file for values classes""" - -import pytest - -from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import BaseValue, ClearValue, EncryptedValue - - -@pytest.mark.parametrize( - "value,expected_repr_str", - [ - pytest.param( - ClearValue(Integer(8, is_signed=False)), - "ClearValue>", - id="ClearValue 8 bits unsigned Integer", - ), - pytest.param( - EncryptedValue(Integer(8, is_signed=True)), - "EncryptedValue>", - id="EncryptedValue 8 bits signed Integer", - ), - ], -) -def test_values_repr(value: BaseValue, expected_repr_str: str): - """Test value repr""" - assert value.__repr__() == expected_repr_str From 0ff3ae47959bcf46701fa4c65a06e90d9d805758 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 14:40:50 +0200 Subject: [PATCH 0112/1104] refactor: refactor BaseValue and mix_values_determine_holding_dtype - add _is_encrypted to BaseValue - remove EncryptedValue and ClearValue classes - add a ScalarValue class - add two helpers EncryptedValue and ClearValue which create a ScalarValue either encrypted or not when passed a data_type - rename to mix_scalar_values_determine_holding_dtype - change typing --- hdk/common/data_types/dtypes_helpers.py | 41 +++++++++++-------- hdk/common/data_types/values.py | 41 +++++++++++++++---- hdk/common/representation/intermediate.py | 4 +- .../common/data_types/test_dtypes_helpers.py | 4 +- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 3d289205e..731eb54d1 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -6,7 +6,7 @@ from typing import cast from .base import BaseDataType from .floats import Float from .integers import Integer -from .values import BaseValue, ClearValue, EncryptedValue +from .values import BaseValue, ClearValue, EncryptedValue, ScalarValue INTEGER_TYPES = (Integer,) FLOAT_TYPES = (Float,) @@ -22,8 +22,10 @@ def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is an encrypted value of type Integer """ - return isinstance(value_to_check, EncryptedValue) and isinstance( - value_to_check.data_type, INTEGER_TYPES + return ( + isinstance(value_to_check, BaseValue) + and value_to_check.is_encrypted + and isinstance(value_to_check.data_type, INTEGER_TYPES) ) @@ -51,8 +53,10 @@ def value_is_clear_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is a clear value of type Integer """ - return isinstance(value_to_check, ClearValue) and isinstance( - value_to_check.data_type, INTEGER_TYPES + return ( + isinstance(value_to_check, BaseValue) + and value_to_check.is_clear + and isinstance(value_to_check.data_type, INTEGER_TYPES) ) @@ -65,7 +69,9 @@ def value_is_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is a value of type Integer """ - return isinstance(value_to_check.data_type, INTEGER_TYPES) + return isinstance(value_to_check, BaseValue) and isinstance( + value_to_check.data_type, INTEGER_TYPES + ) def find_type_to_hold_both_lossy( @@ -127,26 +133,29 @@ def find_type_to_hold_both_lossy( return type_to_return -def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> BaseValue: +def mix_scalar_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> ScalarValue: """Return mixed value with data type able to hold both value1 and value2 dtypes. - Returns a Value that would result from computation on both value1 and value2 while + Returns a ScalarValue that would result from computation on both value1 and value2 while determining the data type able to hold both value1 and value2 data type (this can be lossy - with floats) + with floats). Args: - value1 (BaseValue): first value to mix - value2 (BaseValue): second value to mix + value1 (BaseValue): first ScalarValue to mix. + value2 (BaseValue): second ScalarValue to mix. Returns: - BaseValue: The resulting mixed value with data type able to hold both value1 and value2 - dtypes + ScalarValue: The resulting mixed BaseValue with data type able to hold both value1 and + value2 dtypes. """ + + assert isinstance(value1, ScalarValue), f"Unsupported value1: {value1}, expected ScalarValue" + assert isinstance(value2, ScalarValue), f"Unsupported value2: {value2}, expected ScalarValue" + holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) + mixed_value: ScalarValue - mixed_value: BaseValue - - if isinstance(value1, EncryptedValue) or isinstance(value2, EncryptedValue): + if value1.is_encrypted or value2.is_encrypted: mixed_value = EncryptedValue(holding_type) else: mixed_value = ClearValue(holding_type) diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index eda60eeb5..0001b3a83 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -1,6 +1,7 @@ """File holding classes representing values used by an FHE program.""" -from abc import ABC +from abc import ABC, abstractmethod +from functools import partial from . import base @@ -9,20 +10,46 @@ class BaseValue(ABC): """Abstract base class to represent any kind of value in a program.""" data_type: base.BaseDataType + _is_encrypted: bool - def __init__(self, data_type: base.BaseDataType) -> None: + def __init__(self, data_type: base.BaseDataType, is_encrypted: bool) -> None: self.data_type = data_type + self._is_encrypted = is_encrypted def __repr__(self) -> str: # pragma: no cover - return f"{self.__class__.__name__}<{self.data_type!r}>" + encrypted_str = "Encrypted" if self._is_encrypted else "Clear" + return f"{encrypted_str}{self.__class__.__name__}<{self.data_type!r}>" + @abstractmethod def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and self.data_type == other.data_type + @property + def is_encrypted(self) -> bool: + """Whether Value is encrypted or not. -class ClearValue(BaseValue): - """Class representing a clear/plaintext value (constant or not).""" + Returns: + bool: True if encrypted False otherwise + """ + return self._is_encrypted + + @property + def is_clear(self) -> bool: + """Whether Value is clear or not. + + Returns: + bool: True if clear False otherwise + """ + return not self._is_encrypted -class EncryptedValue(BaseValue): - """Class representing an encrypted value (constant or not).""" +class ScalarValue(BaseValue): + """Class representing a scalar value.""" + + def __eq__(self, other: object) -> bool: + return BaseValue.__eq__(self, other) + + +ClearValue = partial(ScalarValue, is_encrypted=False) + +EncryptedValue = partial(ScalarValue, is_encrypted=True) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index e257556d5..13903685a 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -6,7 +6,7 @@ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple from ..data_types import BaseValue from ..data_types.base import BaseDataType -from ..data_types.dtypes_helpers import mix_values_determine_holding_dtype +from ..data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype from ..data_types.floats import Float from ..data_types.integers import Integer, get_bits_to_represent_int from ..data_types.scalars import Scalars @@ -35,7 +35,7 @@ class IntermediateNode(ABC): assert len(self.inputs) == 2 - self.outputs = [mix_values_determine_holding_dtype(self.inputs[0], self.inputs[1])] + self.outputs = [mix_scalar_values_determine_holding_dtype(self.inputs[0], self.inputs[1])] def _is_equivalent_to_binary_commutative(self, other: object) -> bool: """is_equivalent_to for a binary and commutative operation.""" diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index c66fbae88..1a4761330 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -5,7 +5,7 @@ import pytest from hdk.common.data_types.base import BaseDataType from hdk.common.data_types.dtypes_helpers import ( find_type_to_hold_both_lossy, - mix_values_determine_holding_dtype, + mix_scalar_values_determine_holding_dtype, value_is_encrypted_integer, value_is_encrypted_unsigned_integer, ) @@ -167,4 +167,4 @@ def test_mix_data_types( def test_mix_values(value1: BaseValue, value2: BaseValue, expected_mixed_value: BaseValue): """Test mix_values helper""" - assert expected_mixed_value == mix_values_determine_holding_dtype(value1, value2) + assert expected_mixed_value == mix_scalar_values_determine_holding_dtype(value1, value2) From 60daf3198165c3dbf2ab9b781e522e544d115488 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 15:06:09 +0200 Subject: [PATCH 0113/1104] refactor: refactor make_integer_to_hold_ints - rename make_integer_to_hold_ints to make_integer_to_hold - accept any values as input as we don't know which type this function will be called with - rename get_bits_to_represent_int to get_bits_to_represent_value_as_integer --- hdk/common/data_types/integers.py | 19 +++++++++---------- hdk/common/extensions/table.py | 4 ++-- hdk/common/operator_graph.py | 6 +++--- hdk/common/representation/intermediate.py | 9 +++++++-- tests/common/data_types/test_integers.py | 4 ++-- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/hdk/common/data_types/integers.py b/hdk/common/data_types/integers.py index dab19380a..7e5b0d79b 100644 --- a/hdk/common/data_types/integers.py +++ b/hdk/common/data_types/integers.py @@ -1,7 +1,7 @@ """This file holds the definitions for integer types.""" import math -from typing import Iterable +from typing import Any, Iterable from . import base @@ -84,36 +84,35 @@ def create_unsigned_integer(bit_width: int) -> Integer: UnsignedInteger = create_unsigned_integer -def make_integer_to_hold_ints(values: Iterable[int], force_signed: bool) -> Integer: +def make_integer_to_hold(values: Iterable[Any], force_signed: bool) -> Integer: """Returns an Integer able to hold all values, it is possible to force the Integer to be signed. Args: - values (Iterable[int]): The values to hold + values (Iterable[Any]): The values to hold force_signed (bool): Set to True to force the result to be a signed Integer Returns: Integer: The Integer able to hold values """ - assert all(isinstance(x, int) for x in values) min_value = min(values) max_value = max(values) make_signed_integer = force_signed or min_value < 0 num_bits = max( - get_bits_to_represent_int(min_value, make_signed_integer), - get_bits_to_represent_int(max_value, make_signed_integer), + get_bits_to_represent_value_as_integer(min_value, make_signed_integer), + get_bits_to_represent_value_as_integer(max_value, make_signed_integer), ) return Integer(num_bits, is_signed=make_signed_integer) -def get_bits_to_represent_int(value: int, force_signed: bool) -> int: - """Returns how many bits are required to represent a single int. +def get_bits_to_represent_value_as_integer(value: Any, force_signed: bool) -> int: + """Returns how many bits are required to represent a numerical Value. Args: - value (int): The int for which we want to know how many bits are required - force_signed (bool): Set to True to force the result to be a signed Integer + value (Any): The value for which we want to know how many bits are required. + force_signed (bool): Set to True to force the result to be a signed integer. Returns: int: required amount of bits diff --git a/hdk/common/extensions/table.py b/hdk/common/extensions/table.py index 74845799d..5326a8f6c 100644 --- a/hdk/common/extensions/table.py +++ b/hdk/common/extensions/table.py @@ -5,7 +5,7 @@ from typing import Iterable, Tuple, Union from ..common_helpers import is_a_power_of_2 from ..data_types.base import BaseDataType -from ..data_types.integers import make_integer_to_hold_ints +from ..data_types.integers import make_integer_to_hold from ..representation import intermediate as ir from ..tracing.base_tracer import BaseTracer @@ -28,7 +28,7 @@ class LookupTable: ) self.table = table - self.output_dtype = make_integer_to_hold_ints(table, force_signed=False) + self.output_dtype = make_integer_to_hold(table, force_signed=False) def __getitem__(self, key: Union[int, BaseTracer]): # if a tracer is used for indexing, diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index eecc4ac85..e0015f5e8 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Iterable, List, Set, Tuple, Union import networkx as nx from .data_types.floats import Float -from .data_types.integers import make_integer_to_hold_ints +from .data_types.integers import make_integer_to_hold from .representation import intermediate as ir from .tracing import BaseTracer from .tracing.tracing_helpers import create_graph_from_output_tracers @@ -152,7 +152,7 @@ class OPGraph: if not isinstance(node, ir.Input): for output_value in node.outputs: if isinstance(min_bound, int) and isinstance(max_bound, int): - output_value.data_type = make_integer_to_hold_ints( + output_value.data_type = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) else: @@ -163,7 +163,7 @@ class OPGraph: f"Inputs to a graph should be integers, got bounds that were not float, \n" f"min: {min_bound} ({type(min_bound)}), max: {max_bound} ({type(max_bound)})" ) - node.inputs[0].data_type = make_integer_to_hold_ints( + node.inputs[0].data_type = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) node.outputs[0] = deepcopy(node.inputs[0]) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 13903685a..fecc54d58 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -8,7 +8,7 @@ from ..data_types import BaseValue from ..data_types.base import BaseDataType from ..data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype from ..data_types.floats import Float -from ..data_types.integers import Integer, get_bits_to_represent_int +from ..data_types.integers import Integer, get_bits_to_represent_value_as_integer from ..data_types.scalars import Scalars from ..data_types.values import ClearValue, EncryptedValue @@ -162,7 +162,12 @@ class ConstantInput(IntermediateNode): if isinstance(constant_data, int): is_signed = constant_data < 0 self.outputs = [ - ClearValue(Integer(get_bits_to_represent_int(constant_data, is_signed), is_signed)) + ClearValue( + Integer( + get_bits_to_represent_value_as_integer(constant_data, is_signed), + is_signed, + ) + ) ] elif isinstance(constant_data, float): self.outputs = [ClearValue(Float(64))] diff --git a/tests/common/data_types/test_integers.py b/tests/common/data_types/test_integers.py index c4ed8f1fc..7d1f70a2b 100644 --- a/tests/common/data_types/test_integers.py +++ b/tests/common/data_types/test_integers.py @@ -8,7 +8,7 @@ from hdk.common.data_types.integers import ( Integer, SignedInteger, UnsignedInteger, - make_integer_to_hold_ints, + make_integer_to_hold, ) @@ -109,4 +109,4 @@ def test_integers_repr(integer: Integer, expected_repr_str: str): ) def test_make_integer_to_hold(values, force_signed, expected_result): """Test make_integer_to_hold""" - assert expected_result == make_integer_to_hold_ints(values, force_signed) + assert expected_result == make_integer_to_hold(values, force_signed) From 4f103e604a7ea9effe06a20f46789b19e1641723 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 15:11:11 +0200 Subject: [PATCH 0114/1104] refactor: update OPGraph to be able to update bounds with foreign types - get BaseDataType for values being checked in update_values_with_bounds - rename SUPPORTED_TYPES to BASE_DATA_TYPES --- hdk/common/data_types/dtypes_helpers.py | 34 +++++++++++++++++++++---- hdk/common/operator_graph.py | 27 +++++++++++++++----- hdk/hnumpy/np_dtypes_helpers.py | 4 +-- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 731eb54d1..6521ba12e 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -1,16 +1,16 @@ """File to hold helper functions for data types related stuff.""" from copy import deepcopy -from typing import cast +from typing import Union, cast from .base import BaseDataType from .floats import Float -from .integers import Integer +from .integers import Integer, get_bits_to_represent_value_as_integer from .values import BaseValue, ClearValue, EncryptedValue, ScalarValue INTEGER_TYPES = (Integer,) FLOAT_TYPES = (Float,) -SUPPORTED_TYPES = INTEGER_TYPES + FLOAT_TYPES +BASE_DATA_TYPES = INTEGER_TYPES + FLOAT_TYPES def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: @@ -93,8 +93,8 @@ def find_type_to_hold_both_lossy( Returns: BaseDataType: The dtype able to hold (potentially lossy) dtype1 and dtype2 """ - assert isinstance(dtype1, SUPPORTED_TYPES), f"Unsupported dtype1: {type(dtype1)}" - assert isinstance(dtype2, SUPPORTED_TYPES), f"Unsupported dtype2: {type(dtype2)}" + assert isinstance(dtype1, BASE_DATA_TYPES), f"Unsupported dtype1: {type(dtype1)}" + assert isinstance(dtype2, BASE_DATA_TYPES), f"Unsupported dtype2: {type(dtype2)}" type_to_return: BaseDataType @@ -161,3 +161,27 @@ def mix_scalar_values_determine_holding_dtype(value1: BaseValue, value2: BaseVal mixed_value = ClearValue(holding_type) return mixed_value + + +def get_base_data_type_for_python_constant_data(constant_data: Union[int, float]) -> BaseDataType: + """Helper function to determine the BaseDataType to hold the input constant data. + + Args: + constant_data (Union[int, float]): The constant data for which to determine the + corresponding BaseDataType. + + Returns: + BaseDataType: The corresponding BaseDataType + """ + constant_data_type: BaseDataType + assert isinstance( + constant_data, (int, float) + ), f"Unsupported constant data of type {type(constant_data)}" + if isinstance(constant_data, int): + is_signed = constant_data < 0 + constant_data_type = Integer( + get_bits_to_represent_value_as_integer(constant_data, is_signed), is_signed + ) + elif isinstance(constant_data, float): + constant_data_type = Float(64) + return constant_data_type diff --git a/hdk/common/operator_graph.py b/hdk/common/operator_graph.py index e0015f5e8..d42a15419 100644 --- a/hdk/common/operator_graph.py +++ b/hdk/common/operator_graph.py @@ -1,12 +1,14 @@ """Code to wrap and make manipulating networkx graphs easier.""" from copy import deepcopy -from typing import Any, Dict, Iterable, List, Set, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Union import networkx as nx +from .data_types.base import BaseDataType +from .data_types.dtypes_helpers import get_base_data_type_for_python_constant_data from .data_types.floats import Float -from .data_types.integers import make_integer_to_hold +from .data_types.integers import Integer, make_integer_to_hold from .representation import intermediate as ir from .tracing import BaseTracer from .tracing.tracing_helpers import create_graph_from_output_tracers @@ -130,7 +132,13 @@ class OPGraph: return node_results - def update_values_with_bounds(self, node_bounds: dict): + def update_values_with_bounds( + self, + node_bounds: dict, + get_base_data_type_for_constant_data: Callable[ + [Any], BaseDataType + ] = get_base_data_type_for_python_constant_data, + ): """Update values with bounds. Update nodes inputs and outputs values with data types able to hold data ranges measured @@ -139,6 +147,10 @@ class OPGraph: Args: node_bounds (dict): Dictionary with nodes as keys, holding dicts with a 'min' and 'max' keys. Those bounds will be taken as the data range to be represented, per node. + get_base_data_type_for_constant_data (Callable[ [Type], BaseDataType ], optional): This + is a callback function to convert data encountered during value updates to + BaseDataType. This allows to manage data coming from foreign frameworks without + specialising OPGraph. Defaults to get_base_data_type_for_python_constant_data. """ node: ir.IntermediateNode @@ -149,9 +161,12 @@ class OPGraph: current_node_bounds["max"], ) + min_data_type = get_base_data_type_for_constant_data(min_bound) + max_data_type = get_base_data_type_for_constant_data(max_bound) + if not isinstance(node, ir.Input): for output_value in node.outputs: - if isinstance(min_bound, int) and isinstance(max_bound, int): + if isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer): output_value.data_type = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) @@ -159,8 +174,8 @@ class OPGraph: output_value.data_type = Float(64) else: # Currently variable inputs are only allowed to be integers - assert isinstance(min_bound, int) and isinstance(max_bound, int), ( - f"Inputs to a graph should be integers, got bounds that were not float, \n" + assert isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer), ( + f"Inputs to a graph should be integers, got bounds that were float, \n" f"min: {min_bound} ({type(min_bound)}), max: {max_bound} ({type(max_bound)})" ) node.inputs[0].data_type = make_integer_to_hold( diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 0830841d3..1de2d6c4b 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -7,7 +7,7 @@ import numpy from numpy.typing import DTypeLike from ..common.data_types.base import BaseDataType -from ..common.data_types.dtypes_helpers import SUPPORTED_TYPES +from ..common.data_types.dtypes_helpers import BASE_DATA_TYPES from ..common.data_types.floats import Float from ..common.data_types.integers import Integer @@ -62,7 +62,7 @@ def convert_common_dtype_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dty numpy.dtype: The resulting numpy.dtype """ assert isinstance( - common_dtype, SUPPORTED_TYPES + common_dtype, BASE_DATA_TYPES ), f"Unsupported common_dtype: {type(common_dtype)}" type_to_return: numpy.dtype From a4181afe4d45fd5862b0f70768ebb90752bcb165 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 15:41:30 +0200 Subject: [PATCH 0115/1104] refactor: refactor np_dtype_helpers.py - rename convert_numpy_dtype_to_common_dtype to convert_numpy_dtype_to_base_data_type - change names of constants to be clearer --- hdk/hnumpy/np_dtypes_helpers.py | 21 +++++++++++---------- hdk/hnumpy/tracing.py | 6 +++--- tests/hnumpy/test_np_dtypes_helpers.py | 12 ++++++------ 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 1de2d6c4b..4ae4d5c11 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -1,7 +1,7 @@ """File to hold code to manage package and numpy dtypes.""" from copy import deepcopy -from typing import List +from typing import Dict, List import numpy from numpy.typing import DTypeLike @@ -11,7 +11,7 @@ from ..common.data_types.dtypes_helpers import BASE_DATA_TYPES from ..common.data_types.floats import Float from ..common.data_types.integers import Integer -NUMPY_TO_HDK_TYPE_MAPPING = { +NUMPY_TO_HDK_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.int32): Integer(32, is_signed=True), numpy.dtype(numpy.int64): Integer(64, is_signed=True), numpy.dtype(numpy.uint32): Integer(32, is_signed=False), @@ -20,13 +20,14 @@ NUMPY_TO_HDK_TYPE_MAPPING = { numpy.dtype(numpy.float64): Float(64), } -SUPPORTED_NUMPY_TYPES_SET = set(NUMPY_TO_HDK_TYPE_MAPPING.keys()) +SUPPORTED_NUMPY_DTYPES = tuple(NUMPY_TO_HDK_DTYPE_MAPPING) +SUPPORTED_NUMPY_DTYPES_CLASS_TYPES = tuple(dtype.type for dtype in NUMPY_TO_HDK_DTYPE_MAPPING) -SUPPORTED_TYPE_MSG_STRING = ", ".join(sorted(str(dtype) for dtype in SUPPORTED_NUMPY_TYPES_SET)) +SUPPORTED_DTYPE_MSG_STRING = ", ".join(sorted(str(dtype) for dtype in SUPPORTED_NUMPY_DTYPES)) -def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: - """Helper function to get the corresponding type from a numpy dtype. +def convert_numpy_dtype_to_base_data_type(numpy_dtype: DTypeLike) -> BaseDataType: + """Helper function to get the corresponding BaseDataType from a numpy dtype. Args: numpy_dtype (DTypeLike): Any python object that can be translated to a numpy.dtype @@ -39,20 +40,20 @@ def convert_numpy_dtype_to_common_dtype(numpy_dtype: DTypeLike) -> BaseDataType: """ # Normalize numpy_dtype normalized_numpy_dtype = numpy.dtype(numpy_dtype) - corresponding_hdk_dtype = NUMPY_TO_HDK_TYPE_MAPPING.get(normalized_numpy_dtype, None) + corresponding_hdk_dtype = NUMPY_TO_HDK_DTYPE_MAPPING.get(normalized_numpy_dtype, None) if corresponding_hdk_dtype is None: raise ValueError( f"Unsupported numpy type: {numpy_dtype} ({normalized_numpy_dtype}), " f"supported numpy types: " - f"{SUPPORTED_TYPE_MSG_STRING}" + f"{SUPPORTED_DTYPE_MSG_STRING}" ) # deepcopy to avoid having the value from the dict modified return deepcopy(corresponding_hdk_dtype) -def convert_common_dtype_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dtype: +def convert_base_data_type_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dtype: """Convert a BaseDataType to corresponding numpy.dtype. Args: @@ -109,7 +110,7 @@ def get_ufunc_numpy_output_dtype( len(input_dtypes) == ufunc.nin ), f"Expected {ufunc.nin} types, got {len(input_dtypes)}: {input_dtypes}" - input_numpy_dtypes = [convert_common_dtype_to_numpy_dtype(dtype) for dtype in input_dtypes] + input_numpy_dtypes = [convert_base_data_type_to_numpy_dtype(dtype) for dtype in input_dtypes] # Store numpy old error settings and ignore all errors in this function # We ignore errors as we may call functions with invalid inputs just to get the proper output diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 74416b2c7..ac9251929 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -10,7 +10,7 @@ from ..common.operator_graph import OPGraph from ..common.representation import intermediate as ir from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from .np_dtypes_helpers import ( - convert_numpy_dtype_to_common_dtype, + convert_numpy_dtype_to_base_data_type, get_ufunc_numpy_output_dtype, ) @@ -49,7 +49,7 @@ class NPTracer(BaseTracer): ), f"astype currently only supports tracing without **kwargs, got {kwargs}" normalized_numpy_dtype = numpy.dtype(numpy_dtype) - output_dtype = convert_numpy_dtype_to_common_dtype(numpy_dtype) + output_dtype = convert_numpy_dtype_to_base_data_type(numpy_dtype) traced_computation = ir.ArbitraryFunction( input_base_value=self.output, arbitrary_func=normalized_numpy_dtype.type, @@ -87,7 +87,7 @@ class NPTracer(BaseTracer): ufunc, [input_tracer.output.data_type for input_tracer in input_tracers] ) common_output_dtypes = [ - convert_numpy_dtype_to_common_dtype(dtype) for dtype in output_dtypes + convert_numpy_dtype_to_base_data_type(dtype) for dtype in output_dtypes ] return common_output_dtypes diff --git a/tests/hnumpy/test_np_dtypes_helpers.py b/tests/hnumpy/test_np_dtypes_helpers.py index 269982390..e1433e19c 100644 --- a/tests/hnumpy/test_np_dtypes_helpers.py +++ b/tests/hnumpy/test_np_dtypes_helpers.py @@ -6,8 +6,8 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.hnumpy.np_dtypes_helpers import ( - convert_common_dtype_to_numpy_dtype, - convert_numpy_dtype_to_common_dtype, + convert_base_data_type_to_numpy_dtype, + convert_numpy_dtype_to_base_data_type, ) @@ -29,9 +29,9 @@ from hdk.hnumpy.np_dtypes_helpers import ( pytest.param("complex64", None, marks=pytest.mark.xfail(strict=True, raises=ValueError)), ], ) -def test_convert_numpy_dtype_to_common_dtype(numpy_dtype, expected_common_type): - """Test function for convert_numpy_dtype_to_common_dtype""" - assert convert_numpy_dtype_to_common_dtype(numpy_dtype) == expected_common_type +def test_convert_numpy_dtype_to_base_data_type(numpy_dtype, expected_common_type): + """Test function for convert_numpy_dtype_to_base_data_type""" + assert convert_numpy_dtype_to_base_data_type(numpy_dtype) == expected_common_type @pytest.mark.parametrize( @@ -54,4 +54,4 @@ def test_convert_numpy_dtype_to_common_dtype(numpy_dtype, expected_common_type): ) def test_convert_common_dtype_to_numpy_dtype(common_dtype, expected_numpy_dtype): """Test function for convert_common_dtype_to_numpy_dtype""" - assert expected_numpy_dtype == convert_common_dtype_to_numpy_dtype(common_dtype) + assert expected_numpy_dtype == convert_base_data_type_to_numpy_dtype(common_dtype) From 504899270712a471aa0ae5fd05e0c58db8f29129 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 16:10:40 +0200 Subject: [PATCH 0116/1104] refactor: properly manage unhandled operands in BaseTracer and NPTracer - use the pythonic way of signaling unhandled operands - change typing hint in NPTracer --- hdk/common/tracing/base_tracer.py | 28 ++++++++++++++++++++++++++-- hdk/hnumpy/tracing.py | 14 ++++++++++++-- tests/hnumpy/test_tracing.py | 23 +++++++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 8549f3a3d..950a9a764 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -1,7 +1,7 @@ """This file holds the code that can be shared between tracers.""" -from abc import ABC -from typing import Iterable, List, Tuple, Type, Union +from abc import ABC, abstractmethod +from typing import Any, Iterable, List, Tuple, Type, Union from ..data_types import BaseValue from ..data_types.scalars import Scalars @@ -25,6 +25,18 @@ class BaseTracer(ABC): self.traced_computation = traced_computation self.output = traced_computation.outputs[output_index] + @abstractmethod + def _supports_other_operand(self, other: Any) -> bool: + """Function to check if the current class supports tracing with the other operand. + + Args: + other (Any): the operand to check compatibility with. + + Returns: + bool: True if the tracer can manage operations with the other operand. + """ + return isinstance(other, self.__class__) + def instantiate_output_tracers( self, inputs: Iterable[Union["BaseTracer", Scalars]], @@ -60,6 +72,9 @@ class BaseTracer(ABC): return output_tracers def __add__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + if not self._supports_other_operand(other): + return NotImplemented + result_tracer = self.instantiate_output_tracers( [self, other], ir.Add, @@ -74,6 +89,9 @@ class BaseTracer(ABC): __radd__ = __add__ def __sub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + if not self._supports_other_operand(other): + return NotImplemented + result_tracer = self.instantiate_output_tracers( [self, other], ir.Sub, @@ -83,6 +101,9 @@ class BaseTracer(ABC): return result_tracer[0] def __rsub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + if not self._supports_other_operand(other): + return NotImplemented + result_tracer = self.instantiate_output_tracers( [other, self], ir.Sub, @@ -92,6 +113,9 @@ class BaseTracer(ABC): return result_tracer[0] def __mul__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + if not self._supports_other_operand(other): + return NotImplemented + result_tracer = self.instantiate_output_tracers( [self, other], ir.Mul, diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index ac9251929..a01fb5bd7 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,6 +1,6 @@ """hnumpy tracing utilities.""" from copy import deepcopy -from typing import Callable, Dict, Mapping +from typing import Any, Callable, Dict import numpy from numpy.typing import DTypeLike @@ -10,10 +10,15 @@ from ..common.operator_graph import OPGraph from ..common.representation import intermediate as ir from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from .np_dtypes_helpers import ( + SUPPORTED_NUMPY_DTYPES_CLASS_TYPES, convert_numpy_dtype_to_base_data_type, get_ufunc_numpy_output_dtype, ) +SUPPORTED_TYPES_FOR_TRACING = (int, float, numpy.ndarray) + tuple( + SUPPORTED_NUMPY_DTYPES_CLASS_TYPES +) + class NPTracer(BaseTracer): """Tracer class for numpy operations.""" @@ -81,6 +86,11 @@ class NPTracer(BaseTracer): ) return tracing_func + def _supports_other_operand(self, other: Any) -> bool: + return super()._supports_other_operand(other) or isinstance( + other, SUPPORTED_TYPES_FOR_TRACING + ) + @staticmethod def _manage_dtypes(ufunc: numpy.ufunc, *input_tracers: "NPTracer"): output_dtypes = get_ufunc_numpy_output_dtype( @@ -135,7 +145,7 @@ class NPTracer(BaseTracer): ) return output_tracer - UFUNC_ROUTING: Mapping[numpy.ufunc, Callable] = { + UFUNC_ROUTING: Dict[numpy.ufunc, Callable] = { numpy.rint: rint, numpy.sin: sin, } diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 3ede097c9..8d7e25ded 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -280,3 +280,26 @@ def test_trace_hnumpy_supported_ufuncs( def test_nptracer_get_tracing_func_for_np_ufunc(np_ufunc, expected_tracing_func): """Test NPTracer get_tracing_func_for_np_ufunc""" assert tracing.NPTracer.get_tracing_func_for_np_ufunc(np_ufunc) == expected_tracing_func + + +@pytest.mark.parametrize( + "tracer", + [ + tracing.NPTracer([], ir.Input(ClearValue(Integer(32, True)), "x", 0), 0), + ], +) +@pytest.mark.parametrize( + "operation", + [ + lambda x: x + "fail", + lambda x: "fail" + x, + lambda x: x - "fail", + lambda x: "fail" - x, + lambda x: x * "fail", + lambda x: "fail" * x, + ], +) +def test_nptracer_unsupported_operands(operation, tracer): + """Test cases where NPTracer cannot be used with other operands.""" + with pytest.raises(TypeError): + tracer = operation(tracer) From 5e258ca443f58f08e03ebe215efe9857945157eb Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 16:25:58 +0200 Subject: [PATCH 0117/1104] refactor: change type hints in IntermediateNode evaluate --- hdk/common/representation/intermediate.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index fecc54d58..4eeebbe6d 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from copy import deepcopy -from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple from ..data_types import BaseValue from ..data_types.base import BaseDataType @@ -73,11 +73,11 @@ class IntermediateNode(ABC): ) @abstractmethod - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: """Function to simulate what the represented computation would output for the given inputs. Args: - inputs (Mapping[int, Any]): Mapping containing the inputs for the evaluation + inputs (Dict[int, Any]): Dict containing the inputs for the evaluation Returns: Any: the result of the computation @@ -90,7 +90,7 @@ class Add(IntermediateNode): __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] + inputs[1] @@ -100,7 +100,7 @@ class Sub(IntermediateNode): __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_non_commutative - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] - inputs[1] @@ -110,7 +110,7 @@ class Mul(IntermediateNode): __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] * inputs[1] @@ -132,7 +132,7 @@ class Input(IntermediateNode): self.program_input_idx = program_input_idx self.outputs = [deepcopy(self.inputs[0])] - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] def is_equivalent_to(self, other: object) -> bool: @@ -172,7 +172,7 @@ class ConstantInput(IntermediateNode): elif isinstance(constant_data, float): self.outputs = [ClearValue(Float(64))] - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: return self.constant_data def is_equivalent_to(self, other: object) -> bool: @@ -211,7 +211,7 @@ class ArbitraryFunction(IntermediateNode): self.outputs = [EncryptedValue(output_dtype)] self.op_name = op_name if op_name is not None else self.__class__.__name__ - def evaluate(self, inputs: Mapping[int, Any]) -> Any: + def evaluate(self, inputs: Dict[int, Any]) -> Any: # This is the continuation of the mypy bug workaround assert self.arbitrary_func is not None return self.arbitrary_func(inputs[0], *self.op_args, **self.op_kwargs) From c528d72e62c2d34729245e643b525509b96cf04f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 16:33:47 +0200 Subject: [PATCH 0118/1104] refactor: remove Scalars type hint - add a helper function to determine BaseDataType of a constant python scalar, int or float in dtype_helpers.py - make BaseTracer type agnostic - make ConstantInput type agnostic --- hdk/common/data_types/dtypes_helpers.py | 24 ++++++++++++- hdk/common/data_types/scalars.py | 6 ---- hdk/common/representation/intermediate.py | 43 +++++++++++------------ hdk/common/tracing/base_tracer.py | 18 +++++----- 4 files changed, 52 insertions(+), 39 deletions(-) delete mode 100644 hdk/common/data_types/scalars.py diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 6521ba12e..91db9f1f9 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -1,7 +1,8 @@ """File to hold helper functions for data types related stuff.""" from copy import deepcopy -from typing import Union, cast +from functools import partial +from typing import Callable, Union, cast from .base import BaseDataType from .floats import Float @@ -185,3 +186,24 @@ def get_base_data_type_for_python_constant_data(constant_data: Union[int, float] elif isinstance(constant_data, float): constant_data_type = Float(64) return constant_data_type + + +def get_base_value_for_python_constant_data( + constant_data: Union[int, float] +) -> Callable[..., ScalarValue]: + """Function to wrap the BaseDataType to hold the input constant data in a ScalarValue partial. + + The returned object can then be instantiated as an Encrypted or Clear version of the ScalarValue + by calling it with the proper arguments forwarded to the ScalarValue `__init__` function + + Args: + constant_data (Union[int, float]): The constant data for which to determine the + corresponding ScalarValue and BaseDataType. + + Returns: + Callable[..., ScalarValue]: A partial object that will return the proper ScalarValue when + called with `encrypted` as keyword argument (forwarded to the ScalarValue `__init__` + method). + """ + constant_data_type = get_base_data_type_for_python_constant_data(constant_data) + return partial(ScalarValue, data_type=constant_data_type) diff --git a/hdk/common/data_types/scalars.py b/hdk/common/data_types/scalars.py deleted file mode 100644 index 078777cb5..000000000 --- a/hdk/common/data_types/scalars.py +++ /dev/null @@ -1,6 +0,0 @@ -"""File holding code to represent data types used for constants in programs.""" - -from typing import Union - -# TODO: deal with more types -Scalars = Union[int, float] diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 4eeebbe6d..f591ca1cd 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -6,11 +6,11 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple from ..data_types import BaseValue from ..data_types.base import BaseDataType -from ..data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype -from ..data_types.floats import Float -from ..data_types.integers import Integer, get_bits_to_represent_value_as_integer -from ..data_types.scalars import Scalars -from ..data_types.values import ClearValue, EncryptedValue +from ..data_types.dtypes_helpers import ( + get_base_value_for_python_constant_data, + mix_scalar_values_determine_holding_dtype, +) +from ..data_types.values import EncryptedValue class IntermediateNode(ABC): @@ -147,30 +147,18 @@ class Input(IntermediateNode): class ConstantInput(IntermediateNode): """Node representing a constant of the program.""" - constant_data: Scalars + _constant_data: Any def __init__( self, - constant_data: Scalars, + constant_data: Any, ) -> None: super().__init__([]) - self.constant_data = constant_data - assert isinstance( - constant_data, (int, float) - ), "Only int and float are support for constant input" - if isinstance(constant_data, int): - is_signed = constant_data < 0 - self.outputs = [ - ClearValue( - Integer( - get_bits_to_represent_value_as_integer(constant_data, is_signed), - is_signed, - ) - ) - ] - elif isinstance(constant_data, float): - self.outputs = [ClearValue(Float(64))] + base_value_class = get_base_value_for_python_constant_data(constant_data) + + self._constant_data = constant_data + self.outputs = [base_value_class(is_encrypted=False)] def evaluate(self, inputs: Dict[int, Any]) -> Any: return self.constant_data @@ -182,6 +170,15 @@ class ConstantInput(IntermediateNode): and super().is_equivalent_to(other) ) + @property + def constant_data(self) -> Any: + """Returns the constant_data stored in the ConstantInput node. + + Returns: + Any: The constant data that was stored. + """ + return self._constant_data + class ArbitraryFunction(IntermediateNode): """Node representing a univariate arbitrary function, e.g. sin(x).""" diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 950a9a764..83a52af07 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -4,7 +4,6 @@ from abc import ABC, abstractmethod from typing import Any, Iterable, List, Tuple, Type, Union from ..data_types import BaseValue -from ..data_types.scalars import Scalars from ..representation import intermediate as ir @@ -39,13 +38,14 @@ class BaseTracer(ABC): def instantiate_output_tracers( self, - inputs: Iterable[Union["BaseTracer", Scalars]], + inputs: Iterable[Union["BaseTracer", Any]], computation_to_trace: Type[ir.IntermediateNode], ) -> Tuple["BaseTracer", ...]: """Helper functions to instantiate all output BaseTracer for a given computation. Args: - inputs (List[BaseTracer]): Previous BaseTracer used as inputs for a new node + inputs (Iterable[Union[BaseTracer, Any]]): Previous BaseTracer or data used as inputs + for a new node. computation_to_trace (Type[ir.IntermediateNode]): The IntermediateNode class to instantiate for the computation being traced @@ -71,7 +71,7 @@ class BaseTracer(ABC): return output_tracers - def __add__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + def __add__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": if not self._supports_other_operand(other): return NotImplemented @@ -88,7 +88,7 @@ class BaseTracer(ABC): # some changes __radd__ = __add__ - def __sub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + def __sub__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": if not self._supports_other_operand(other): return NotImplemented @@ -100,7 +100,7 @@ class BaseTracer(ABC): assert len(result_tracer) == 1 return result_tracer[0] - def __rsub__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + def __rsub__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": if not self._supports_other_operand(other): return NotImplemented @@ -112,7 +112,7 @@ class BaseTracer(ABC): assert len(result_tracer) == 1 return result_tracer[0] - def __mul__(self, other: Union["BaseTracer", Scalars]) -> "BaseTracer": + def __mul__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": if not self._supports_other_operand(other): return NotImplemented @@ -130,12 +130,12 @@ class BaseTracer(ABC): __rmul__ = __mul__ -def make_const_input_tracer(tracer_class: Type[BaseTracer], constant_data: Scalars) -> BaseTracer: +def make_const_input_tracer(tracer_class: Type[BaseTracer], constant_data: Any) -> BaseTracer: """Helper function to create a tracer for a constant input. Args: tracer_class (Type[BaseTracer]): the class of tracer to create a ConstantInput for - constant_data (Scalars): the constant + constant_data (Any): the constant Returns: BaseTracer: The BaseTracer for that constant From 9a0c108d4b316b4746b1d60b212995b52f5530da Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 16:56:31 +0200 Subject: [PATCH 0119/1104] refactor: refactor ConstantInput to be flexible - refactor to take a function to generate the propore BaseValue to store in its output - refactor BaseTracer to force inheriting tracers to indicate how to build a ConstantInput tracer - remove "as import" for intermediate in hnumpy/tracing.py - update compile to manage python dtypes --- hdk/common/representation/intermediate.py | 5 +- hdk/common/tracing/base_tracer.py | 26 +++++----- hdk/hnumpy/compile.py | 5 +- hdk/hnumpy/np_dtypes_helpers.py | 62 ++++++++++++++++++++++- hdk/hnumpy/tracing.py | 18 +++++-- tests/hnumpy/test_compile.py | 1 + 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index f591ca1cd..b2b26db65 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -152,10 +152,13 @@ class ConstantInput(IntermediateNode): def __init__( self, constant_data: Any, + get_base_value_for_data_func: Callable[ + [Any], Callable[..., BaseValue] + ] = get_base_value_for_python_constant_data, ) -> None: super().__init__([]) - base_value_class = get_base_value_for_python_constant_data(constant_data) + base_value_class = get_base_value_for_data_func(constant_data) self._constant_data = constant_data self.outputs = [base_value_class(is_encrypted=False)] diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 83a52af07..e6774147a 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -36,6 +36,17 @@ class BaseTracer(ABC): """ return isinstance(other, self.__class__) + @abstractmethod + def _make_const_input_tracer(self, constant_data: Any) -> "BaseTracer": + """Helper function to create a tracer for a constant input. + + Args: + constant_data (Any): The constant to store. + + Returns: + BaseTracer: The BaseTracer for that constant. + """ + def instantiate_output_tracers( self, inputs: Iterable[Union["BaseTracer", Any]], @@ -55,7 +66,7 @@ class BaseTracer(ABC): # For inputs which are actually constant, first convert into a tracer def sanitize(inp): if not isinstance(inp, BaseTracer): - return make_const_input_tracer(self.__class__, inp) + return self._make_const_input_tracer(inp) return inp sanitized_inputs = [sanitize(inp) for inp in inputs] @@ -128,16 +139,3 @@ class BaseTracer(ABC): # the order, we need to do as in __rmul__, ie mostly a copy of __mul__ + # some changes __rmul__ = __mul__ - - -def make_const_input_tracer(tracer_class: Type[BaseTracer], constant_data: Any) -> BaseTracer: - """Helper function to create a tracer for a constant input. - - Args: - tracer_class (Type[BaseTracer]): the class of tracer to create a ConstantInput for - constant_data (Any): the constant - - Returns: - BaseTracer: The BaseTracer for that constant - """ - return tracer_class([], ir.ConstantInput(constant_data), 0) diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 1f629b674..04f2fc4e1 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -17,6 +17,7 @@ from ..common.operator_graph import OPGraph from ..common.optimization.topological import fuse_float_operations from ..common.representation import intermediate as ir from ..hnumpy.tracing import trace_numpy_function +from .np_dtypes_helpers import get_base_data_type_for_numpy_or_python_constant_data def compile_numpy_function_into_op_graph( @@ -74,7 +75,9 @@ def compile_numpy_function_into_op_graph( node_bounds = eval_op_graph_bounds_on_dataset(op_graph, dataset) # Update the graph accordingly: after that, we have the compilable graph - op_graph.update_values_with_bounds(node_bounds) + op_graph.update_values_with_bounds( + node_bounds, get_base_data_type_for_numpy_or_python_constant_data + ) # Make sure the graph can be lowered to MLIR if not is_graph_values_compatible_with_mlir(op_graph): diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 4ae4d5c11..82d1ebb00 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -1,15 +1,21 @@ """File to hold code to manage package and numpy dtypes.""" from copy import deepcopy -from typing import Dict, List +from functools import partial +from typing import Any, Callable, Dict, List import numpy from numpy.typing import DTypeLike from ..common.data_types.base import BaseDataType -from ..common.data_types.dtypes_helpers import BASE_DATA_TYPES +from ..common.data_types.dtypes_helpers import ( + BASE_DATA_TYPES, + get_base_data_type_for_python_constant_data, + get_base_value_for_python_constant_data, +) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer +from ..common.data_types.values import BaseValue, ScalarValue NUMPY_TO_HDK_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.int32): Integer(32, is_signed=True), @@ -92,6 +98,58 @@ def convert_base_data_type_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.d return type_to_return +def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> BaseDataType: + """Helper function to determine the BaseDataType to hold the input constant data. + + Args: + constant_data (Any): The constant data for which to determine the + corresponding BaseDataType. + + Returns: + BaseDataType: The corresponding BaseDataType + """ + base_dtype: BaseDataType + assert isinstance( + constant_data, (int, float, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) + ), f"Unsupported constant data of type {type(constant_data)}" + if isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): + base_dtype = convert_numpy_dtype_to_base_data_type(constant_data) + else: + base_dtype = get_base_data_type_for_python_constant_data(constant_data) + return base_dtype + + +def get_base_value_for_numpy_or_python_constant_data( + constant_data: Any, +) -> Callable[..., BaseValue]: + """Helper function to determine the BaseValue and BaseDataType to hold the input constant data. + + This function is able to handle numpy types + + Args: + constant_data (Any): The constant data for which to determine the + corresponding BaseValue and BaseDataType. + + Raises: + AssertionError: If `constant_data` is of an unsupported type. + + Returns: + Callable[..., BaseValue]: A partial object that will return the proper BaseValue when called + with `encrypted` as keyword argument (forwarded to the BaseValue `__init__` method). + """ + constant_data_value: Callable[..., BaseValue] + assert isinstance( + constant_data, (int, float, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) + ), f"Unsupported constant data of type {type(constant_data)}" + + base_dtype = get_base_data_type_for_numpy_or_python_constant_data(constant_data) + if isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): + constant_data_value = partial(ScalarValue, data_type=base_dtype) + else: + constant_data_value = get_base_value_for_python_constant_data(constant_data) + return constant_data_value + + def get_ufunc_numpy_output_dtype( ufunc: numpy.ufunc, input_dtypes: List[BaseDataType], diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index a01fb5bd7..075409c38 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,5 +1,6 @@ """hnumpy tracing utilities.""" from copy import deepcopy +from functools import partial from typing import Any, Callable, Dict import numpy @@ -7,11 +8,12 @@ from numpy.typing import DTypeLike from ..common.data_types import BaseValue from ..common.operator_graph import OPGraph -from ..common.representation import intermediate as ir +from ..common.representation.intermediate import ArbitraryFunction, ConstantInput from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from .np_dtypes_helpers import ( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES, convert_numpy_dtype_to_base_data_type, + get_base_value_for_numpy_or_python_constant_data, get_ufunc_numpy_output_dtype, ) @@ -19,6 +21,11 @@ SUPPORTED_TYPES_FOR_TRACING = (int, float, numpy.ndarray) + tuple( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES ) +NPConstantInput = partial( + ConstantInput, + get_base_value_for_data_func=get_base_value_for_numpy_or_python_constant_data, +) + class NPTracer(BaseTracer): """Tracer class for numpy operations.""" @@ -55,7 +62,7 @@ class NPTracer(BaseTracer): normalized_numpy_dtype = numpy.dtype(numpy_dtype) output_dtype = convert_numpy_dtype_to_base_data_type(numpy_dtype) - traced_computation = ir.ArbitraryFunction( + traced_computation = ArbitraryFunction( input_base_value=self.output, arbitrary_func=normalized_numpy_dtype.type, output_dtype=output_dtype, @@ -91,6 +98,9 @@ class NPTracer(BaseTracer): other, SUPPORTED_TYPES_FOR_TRACING ) + def _make_const_input_tracer(self, constant_data: Any) -> "NPTracer": + return self.__class__([], NPConstantInput(constant_data), 0) + @staticmethod def _manage_dtypes(ufunc: numpy.ufunc, *input_tracers: "NPTracer"): output_dtypes = get_ufunc_numpy_output_dtype( @@ -111,7 +121,7 @@ class NPTracer(BaseTracer): common_output_dtypes = self._manage_dtypes(numpy.rint, *input_tracers) assert len(common_output_dtypes) == 1 - traced_computation = ir.ArbitraryFunction( + traced_computation = ArbitraryFunction( input_base_value=input_tracers[0].output, arbitrary_func=numpy.rint, output_dtype=common_output_dtypes[0], @@ -133,7 +143,7 @@ class NPTracer(BaseTracer): common_output_dtypes = self._manage_dtypes(numpy.sin, *input_tracers) assert len(common_output_dtypes) == 1 - traced_computation = ir.ArbitraryFunction( + traced_computation = ArbitraryFunction( input_base_value=input_tracers[0].output, arbitrary_func=numpy.sin, output_dtype=common_output_dtypes[0], diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index f03b0e892..8565b7334 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -71,6 +71,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n "function,input_ranges,list_of_arg_names", [ pytest.param(lambda x: x + 42, ((0, 2),), ["x"]), + pytest.param(lambda x: x + numpy.int32(42), ((0, 2),), ["x"]), pytest.param(lambda x: x * 2, ((0, 2),), ["x"]), pytest.param(lambda x: 8 - x, ((0, 2),), ["x"]), pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), From 78480e5da7579f34de1ad74a160ab8802615628f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 17:14:27 +0200 Subject: [PATCH 0120/1104] refactor: make IR agnostic of clear/encypted values --- hdk/common/representation/intermediate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index b2b26db65..ba60f8c23 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -10,7 +10,6 @@ from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, mix_scalar_values_determine_holding_dtype, ) -from ..data_types.values import EncryptedValue class IntermediateNode(ABC): @@ -207,8 +206,7 @@ class ArbitraryFunction(IntermediateNode): self.arbitrary_func = arbitrary_func self.op_args = op_args if op_args is not None else () self.op_kwargs = op_kwargs if op_kwargs is not None else {} - # TLU/PBS has an encrypted output - self.outputs = [EncryptedValue(output_dtype)] + self.outputs = [input_base_value.__class__(output_dtype, input_base_value.is_encrypted)] self.op_name = op_name if op_name is not None else self.__class__.__name__ def evaluate(self, inputs: Dict[int, Any]) -> Any: From 7a0f11b1b078a2b4eb04d09bc5022dec6e498220 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 19 Aug 2021 17:31:15 +0200 Subject: [PATCH 0121/1104] refactor: change IR and BaseTracer to support any input mixing function - prepares support for tensor types - introduce _n_in and n_in() and requires_mix_values_func() for IntermediateNode to know if they require a function to mix input values and determine output value - update BaseTracer to pass the class _mix_values_func to IntermediateNodes that need it - update NPTracer to stay consistent with current behavior regarding values mixing --- hdk/common/representation/intermediate.py | 35 ++++++++++++++++++++++- hdk/common/tracing/base_tracer.py | 15 +++++++++- hdk/hnumpy/tracing.py | 3 ++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index ba60f8c23..fe3610e63 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -11,16 +11,20 @@ from ..data_types.dtypes_helpers import ( mix_scalar_values_determine_holding_dtype, ) +IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" + class IntermediateNode(ABC): """Abstract Base Class to derive from to represent source program operations.""" inputs: List[BaseValue] outputs: List[BaseValue] + _n_in: int # _n_in indicates how many inputs are required to evaluate the IntermediateNode def __init__( self, inputs: Iterable[BaseValue], + **_kwargs, # This is to be able to feed arbitrary arguments to IntermediateNodes ) -> None: self.inputs = list(inputs) assert all(isinstance(x, BaseValue) for x in self.inputs) @@ -28,13 +32,15 @@ class IntermediateNode(ABC): def _init_binary( self, inputs: Iterable[BaseValue], + mix_values_func: Callable[..., BaseValue] = mix_scalar_values_determine_holding_dtype, + **_kwargs, # Required to conform to __init__ typing ) -> None: """__init__ for a binary operation, ie two inputs.""" IntermediateNode.__init__(self, inputs) assert len(self.inputs) == 2 - self.outputs = [mix_scalar_values_determine_holding_dtype(self.inputs[0], self.inputs[1])] + self.outputs = [mix_values_func(self.inputs[0], self.inputs[1])] def _is_equivalent_to_binary_commutative(self, other: object) -> bool: """is_equivalent_to for a binary and commutative operation.""" @@ -82,10 +88,30 @@ class IntermediateNode(ABC): Any: the result of the computation """ + @classmethod + def n_in(cls) -> int: + """Returns how many inputs the node has. + + Returns: + int: The number of inputs of the node. + """ + return cls._n_in + + @classmethod + def requires_mix_values_func(cls) -> bool: + """Function to determine whether the Class requires a mix_values_func to be built. + + Returns: + bool: True if __init__ expects a mix_values_func argument. + """ + return cls.n_in() > 1 + class Add(IntermediateNode): """Addition between two values.""" + _n_in: int = 2 + __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative @@ -96,6 +122,8 @@ class Add(IntermediateNode): class Sub(IntermediateNode): """Subtraction between two values.""" + _n_in: int = 2 + __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_non_commutative @@ -106,6 +134,8 @@ class Sub(IntermediateNode): class Mul(IntermediateNode): """Multiplication between two values.""" + _n_in: int = 2 + __init__ = IntermediateNode._init_binary is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative @@ -118,6 +148,7 @@ class Input(IntermediateNode): input_name: str program_input_idx: int + _n_in: int = 1 def __init__( self, @@ -147,6 +178,7 @@ class ConstantInput(IntermediateNode): """Node representing a constant of the program.""" _constant_data: Any + _n_in: int = 0 def __init__( self, @@ -191,6 +223,7 @@ class ArbitraryFunction(IntermediateNode): op_args: Tuple[Any, ...] op_kwargs: Dict[str, Any] op_name: str + _n_in: int = 1 def __init__( self, diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index e6774147a..2c9018e54 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -1,10 +1,11 @@ """This file holds the code that can be shared between tracers.""" from abc import ABC, abstractmethod -from typing import Any, Iterable, List, Tuple, Type, Union +from typing import Any, Callable, Iterable, List, Tuple, Type, Union from ..data_types import BaseValue from ..representation import intermediate as ir +from ..representation.intermediate import IR_MIX_VALUES_FUNC_ARG_NAME class BaseTracer(ABC): @@ -13,6 +14,7 @@ class BaseTracer(ABC): inputs: List["BaseTracer"] traced_computation: ir.IntermediateNode output: BaseValue + _mix_values_func: Callable[..., BaseValue] def __init__( self, @@ -47,6 +49,10 @@ class BaseTracer(ABC): BaseTracer: The BaseTracer for that constant. """ + @classmethod + def _get_mix_values_func(cls): + return cls._mix_values_func + def instantiate_output_tracers( self, inputs: Iterable[Union["BaseTracer", Any]], @@ -71,8 +77,15 @@ class BaseTracer(ABC): sanitized_inputs = [sanitize(inp) for inp in inputs] + additional_parameters = ( + {IR_MIX_VALUES_FUNC_ARG_NAME: self._get_mix_values_func()} + if computation_to_trace.requires_mix_values_func() + else {} + ) + traced_computation = computation_to_trace( (x.output for x in sanitized_inputs), + **additional_parameters, ) output_tracers = tuple( diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 075409c38..91a4ac401 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -7,6 +7,7 @@ import numpy from numpy.typing import DTypeLike from ..common.data_types import BaseValue +from ..common.data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype from ..common.operator_graph import OPGraph from ..common.representation.intermediate import ArbitraryFunction, ConstantInput from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters @@ -30,6 +31,8 @@ NPConstantInput = partial( class NPTracer(BaseTracer): """Tracer class for numpy operations.""" + _mix_values_func: Callable[..., BaseValue] = mix_scalar_values_determine_holding_dtype + def __array_ufunc__(self, ufunc, method, *input_tracers, **kwargs): """Catch calls to numpy ufunc and routes them to tracing functions if supported. From e41f77349f70238d67d5852caf2510911d32db0c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 20 Aug 2021 09:53:52 +0200 Subject: [PATCH 0122/1104] refactor: make BaseDataType __eq__ abstract - update test files with dummy dtypes --- hdk/common/data_types/base.py | 6 +++++- tests/common/data_types/test_dtypes_helpers.py | 3 +++ tests/common/test_common_helpers.py | 14 +++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/hdk/common/data_types/base.py b/hdk/common/data_types/base.py index 13ef63fe8..834e75dc9 100644 --- a/hdk/common/data_types/base.py +++ b/hdk/common/data_types/base.py @@ -1,7 +1,11 @@ """File holding code to represent data types in a program.""" -from abc import ABC +from abc import ABC, abstractmethod class BaseDataType(ABC): """Base class to represent a data type.""" + + @abstractmethod + def __eq__(self, o: object) -> bool: + """No default implementation.""" diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 1a4761330..805424c99 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -62,6 +62,9 @@ def test_value_is_encrypted_unsigned_integer(value: BaseValue, expected_result: class UnsupportedDataType(BaseDataType): """Test helper class to represent an UnsupportedDataType""" + def __eq__(self, o: object) -> bool: + return isinstance(o, self.__class__) + @pytest.mark.parametrize( "dtype1,dtype2,expected_mixed_dtype", diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index c0c2aef0c..a0e076a45 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -5,7 +5,7 @@ from copy import deepcopy import pytest from hdk.common import check_op_graph_is_integer_program, is_a_power_of_2 -from hdk.common.data_types.base import BaseDataType +from hdk.common.data_types.floats import Float64 from hdk.common.data_types.integers import Integer from hdk.common.data_types.values import EncryptedValue from hdk.hnumpy.tracing import trace_numpy_function @@ -29,10 +29,6 @@ def test_is_a_power_of_2(x, result): assert is_a_power_of_2(x) == result -class DummyNotInteger(BaseDataType): - """Dummy helper data type class""" - - def test_check_op_graph_is_integer_program(): """Test function for check_op_graph_is_integer_program""" @@ -50,7 +46,7 @@ def test_check_op_graph_is_integer_program(): assert len(offending_nodes) == 0 op_graph_copy = deepcopy(op_graph) - op_graph_copy.output_nodes[0].outputs[0].data_type = DummyNotInteger() + op_graph_copy.output_nodes[0].outputs[0].data_type = Float64 offending_nodes = [] assert not check_op_graph_is_integer_program(op_graph_copy) @@ -59,7 +55,7 @@ def test_check_op_graph_is_integer_program(): assert offending_nodes == [op_graph_copy.output_nodes[0]] op_graph_copy = deepcopy(op_graph) - op_graph_copy.input_nodes[0].inputs[0].data_type = DummyNotInteger() + op_graph_copy.input_nodes[0].inputs[0].data_type = Float64 offending_nodes = [] assert not check_op_graph_is_integer_program(op_graph_copy) @@ -68,8 +64,8 @@ def test_check_op_graph_is_integer_program(): assert offending_nodes == [op_graph_copy.input_nodes[0]] op_graph_copy = deepcopy(op_graph) - op_graph_copy.input_nodes[0].inputs[0].data_type = DummyNotInteger() - op_graph_copy.input_nodes[1].inputs[0].data_type = DummyNotInteger() + op_graph_copy.input_nodes[0].inputs[0].data_type = Float64 + op_graph_copy.input_nodes[1].inputs[0].data_type = Float64 offending_nodes = [] assert not check_op_graph_is_integer_program(op_graph_copy) From 0e1534637bb13c2924164627fa464151620faef6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 20 Aug 2021 11:44:31 +0200 Subject: [PATCH 0123/1104] fix: BaseValue now deepcopies the data_type it receives - avoids weird situations with refs to a type that gets modified --- hdk/common/data_types/values.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index 0001b3a83..f1ae0ace7 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -1,6 +1,7 @@ """File holding classes representing values used by an FHE program.""" from abc import ABC, abstractmethod +from copy import deepcopy from functools import partial from . import base @@ -13,7 +14,7 @@ class BaseValue(ABC): _is_encrypted: bool def __init__(self, data_type: base.BaseDataType, is_encrypted: bool) -> None: - self.data_type = data_type + self.data_type = deepcopy(data_type) self._is_encrypted = is_encrypted def __repr__(self) -> str: # pragma: no cover From 985cf973d8254d6c6eb72202170be22df26a2c46 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 20 Aug 2021 11:45:40 +0200 Subject: [PATCH 0124/1104] refactor: values.py - change import for BaseDataType - create helper functions rather than partials to have docstrings in IDE hints --- hdk/common/data_types/values.py | 38 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index f1ae0ace7..0f8e8738d 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -2,18 +2,17 @@ from abc import ABC, abstractmethod from copy import deepcopy -from functools import partial -from . import base +from .base import BaseDataType class BaseValue(ABC): """Abstract base class to represent any kind of value in a program.""" - data_type: base.BaseDataType + data_type: BaseDataType _is_encrypted: bool - def __init__(self, data_type: base.BaseDataType, is_encrypted: bool) -> None: + def __init__(self, data_type: BaseDataType, is_encrypted: bool) -> None: self.data_type = deepcopy(data_type) self._is_encrypted = is_encrypted @@ -51,6 +50,33 @@ class ScalarValue(BaseValue): return BaseValue.__eq__(self, other) -ClearValue = partial(ScalarValue, is_encrypted=False) +def make_clear_scalar( + data_type: BaseDataType, +) -> ScalarValue: + """Helper to create a clear ScalarValue. -EncryptedValue = partial(ScalarValue, is_encrypted=True) + Args: + data_type (BaseDataType): The data type for the value. + + Returns: + ScalarValue: The corresponding ScalarValue. + """ + return ScalarValue(data_type=data_type, is_encrypted=False) + + +def make_encrypted_scalar( + data_type: BaseDataType, +) -> ScalarValue: + """Helper to create an encrypted ScalarValue. + + Args: + data_type (BaseDataType): The data type for the value. + + Returns: + ScalarValue: The corresponding ScalarValue. + """ + return ScalarValue(data_type=data_type, is_encrypted=True) + + +ClearValue = make_clear_scalar +EncryptedValue = make_encrypted_scalar From 4e658c15cb27ef87ef1540a41c4ce54dd71bdcd8 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 20 Aug 2021 11:46:12 +0200 Subject: [PATCH 0125/1104] feat: add TensorValue --- hdk/common/data_types/values.py | 94 ++++++++++++++++++++++++++ tests/common/data_types/test_values.py | 87 ++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 tests/common/data_types/test_values.py diff --git a/hdk/common/data_types/values.py b/hdk/common/data_types/values.py index 0f8e8738d..16c7f409b 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/data_types/values.py @@ -2,6 +2,8 @@ from abc import ABC, abstractmethod from copy import deepcopy +from math import prod +from typing import Optional, Tuple from .base import BaseDataType @@ -80,3 +82,95 @@ def make_encrypted_scalar( ClearValue = make_clear_scalar EncryptedValue = make_encrypted_scalar + + +class TensorValue(BaseValue): + """Class representing a tensor value.""" + + _shape: Tuple[int, ...] + _ndim: int + _size: int + + def __init__( + self, + data_type: BaseDataType, + is_encrypted: bool, + shape: Optional[Tuple[int, ...]] = None, + ) -> None: + super().__init__(data_type, is_encrypted) + # Managing tensors as in numpy, no shape or () is treated as a 0-D array of size 1 + self._shape = shape if shape is not None else () + self._ndim = len(self._shape) + self._size = prod(self._shape) if self._shape else 1 + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and self.shape == other.shape + and self.ndim == other.ndim + and self.size == other.size + and super().__eq__(other) + ) + + @property + def shape(self) -> Tuple[int, ...]: + """The TensorValue shape property. + + Returns: + Tuple[int, ...]: The TensorValue shape. + """ + return self._shape + + @property + def ndim(self) -> int: + """The TensorValue ndim property. + + Returns: + int: The TensorValue ndim. + """ + return self._ndim + + @property + def size(self) -> int: + """The TensorValue size property. + + Returns: + int: The TensorValue size. + """ + return self._size + + +def make_clear_tensor( + data_type: BaseDataType, + shape: Optional[Tuple[int, ...]] = None, +) -> TensorValue: + """Helper to create a clear TensorValue. + + Args: + data_type (BaseDataType): The data type for the tensor. + shape (Optional[Tuple[int, ...]], optional): The tensor shape. Defaults to None. + + Returns: + TensorValue: The corresponding TensorValue. + """ + return TensorValue(data_type=data_type, is_encrypted=False, shape=shape) + + +def make_encrypted_tensor( + data_type: BaseDataType, + shape: Optional[Tuple[int, ...]] = None, +) -> TensorValue: + """Helper to create an encrypted TensorValue. + + Args: + data_type (BaseDataType): The data type for the tensor. + shape (Optional[Tuple[int, ...]], optional): The tensor shape. Defaults to None. + + Returns: + TensorValue: The corresponding TensorValue. + """ + return TensorValue(data_type=data_type, is_encrypted=True, shape=shape) + + +ClearTensor = make_clear_tensor +EncryptedTensor = make_encrypted_tensor diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py new file mode 100644 index 000000000..3f84a34a6 --- /dev/null +++ b/tests/common/data_types/test_values.py @@ -0,0 +1,87 @@ +"""Test file for values related code.""" + +from copy import deepcopy +from functools import partial +from typing import Callable, Optional, Tuple, Union + +import pytest + +from hdk.common.data_types.base import BaseDataType +from hdk.common.data_types.floats import Float +from hdk.common.data_types.integers import Integer +from hdk.common.data_types.values import ClearTensor, EncryptedTensor, TensorValue + + +class DummyDtype(BaseDataType): + """Dummy Helper Dtype""" + + def __eq__(self, o: object) -> bool: + return isinstance(o, self.__class__) + + +@pytest.mark.parametrize( + "tensor_constructor,expected_is_encrypted", + [ + (ClearTensor, False), + (partial(TensorValue, is_encrypted=False), False), + (EncryptedTensor, True), + (partial(TensorValue, is_encrypted=True), True), + ], +) +@pytest.mark.parametrize( + "shape,expected_shape,expected_ndim,expected_size", + [ + (None, (), 0, 1), + ((), (), 0, 1), + ((3, 256, 256), (3, 256, 256), 3, 196_608), + ((1920, 1080, 3), (1920, 1080, 3), 3, 6_220_800), + ], +) +@pytest.mark.parametrize( + "data_type", + [ + Integer(7, False), + Integer(32, True), + Integer(32, False), + Integer(64, True), + Integer(64, False), + Float(32), + Float(64), + ], +) +def test_tensor_value( + tensor_constructor: Callable[..., TensorValue], + expected_is_encrypted: bool, + shape: Optional[Tuple[int, ...]], + expected_shape: Tuple[int, ...], + expected_ndim: int, + expected_size: int, + data_type: Union[Integer, Float], +): + """Test function for TensorValue""" + + tensor_value = tensor_constructor(data_type=data_type, shape=shape) + + assert expected_is_encrypted == tensor_value.is_encrypted + assert expected_shape == tensor_value.shape + assert expected_ndim == tensor_value.ndim + assert expected_size == tensor_value.size + + assert data_type == tensor_value.data_type + + other_tensor = deepcopy(tensor_value) + + assert other_tensor == tensor_value + + other_tensor_value = deepcopy(other_tensor) + other_tensor_value.data_type = DummyDtype() + assert other_tensor_value != tensor_value + + other_shape = tuple(val + 1 for val in shape) if shape is not None else () + other_shape += (2,) + other_tensor_value = tensor_constructor(data_type=data_type, shape=other_shape) + + assert other_tensor_value.shape != tensor_value.shape + assert other_tensor_value.ndim != tensor_value.ndim + assert other_tensor_value.size != tensor_value.size + assert other_tensor_value != tensor_value From 2b5f7f31183ecc40a7d9741398ae0412655d7b9f Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 23 Aug 2021 17:48:25 +0300 Subject: [PATCH 0126/1104] refactor: rename ConstantInput to Constant to reduce confusion --- examples/QuantizedLinearRegression.ipynb | 4 +- examples/QuantizedLogisticRegression.ipynb | 8 ++-- hdk/common/debugging/drawing.py | 4 +- hdk/common/debugging/printing.py | 4 +- hdk/common/mlir/converters.py | 2 +- hdk/common/optimization/topological.py | 12 ++--- hdk/common/representation/intermediate.py | 6 +-- hdk/hnumpy/tracing.py | 8 ++-- tests/common/extensions/test_table.py | 2 +- .../representation/test_intermediate.py | 14 +++--- tests/hnumpy/test_debugging.py | 46 +++++++++---------- 11 files changed, 55 insertions(+), 55 deletions(-) diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 8703a6396..56db26114 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -655,9 +655,9 @@ "output_type": "stream", "text": [ "\n", - "%0 = ConstantInput(1) # Integer\n", + "%0 = Constant(1) # Integer\n", "%1 = x_0 # Integer\n", - "%2 = ConstantInput(15) # Integer\n", + "%2 = Constant(15) # Integer\n", "%3 = Add(1, 2) # Integer\n", "%4 = Mul(3, 0) # Integer\n", "%5 = TLU(4) # Integer\n", diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index be6a880a8..451a7396f 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -762,12 +762,12 @@ "output_type": "stream", "text": [ "\n", - "%0 = ConstantInput(2) # Integer\n", - "%1 = ConstantInput(1) # Integer\n", + "%0 = Constant(2) # Integer\n", + "%1 = Constant(1) # Integer\n", "%2 = x_0 # Integer\n", - "%3 = ConstantInput(6) # Integer\n", + "%3 = Constant(6) # Integer\n", "%4 = x_1 # Integer\n", - "%5 = ConstantInput(6) # Integer\n", + "%5 = Constant(6) # Integer\n", "%6 = Add(2, 3) # Integer\n", "%7 = Add(4, 5) # Integer\n", "%8 = Mul(6, 0) # Integer\n", diff --git a/hdk/common/debugging/drawing.py b/hdk/common/debugging/drawing.py index d430b99f5..c41c648c2 100644 --- a/hdk/common/debugging/drawing.py +++ b/hdk/common/debugging/drawing.py @@ -11,7 +11,7 @@ from ..representation import intermediate as ir IR_NODE_COLOR_MAPPING = { ir.Input: "blue", - ir.ConstantInput: "cyan", + ir.Constant: "cyan", ir.Add: "red", ir.Sub: "yellow", ir.Mul: "green", @@ -151,7 +151,7 @@ def draw_graph( def get_proper_name(node): if isinstance(node, ir.Input): return node.input_name - if isinstance(node, ir.ConstantInput): + if isinstance(node, ir.Constant): return str(node.constant_data) if isinstance(node, ir.ArbitraryFunction): return node.op_name diff --git a/hdk/common/debugging/printing.py b/hdk/common/debugging/printing.py index 713a3a3b8..1cea9033e 100644 --- a/hdk/common/debugging/printing.py +++ b/hdk/common/debugging/printing.py @@ -45,8 +45,8 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: if isinstance(node, ir.Input): what_to_print = node.input_name - elif isinstance(node, ir.ConstantInput): - what_to_print = f"ConstantInput({node.constant_data})" + elif isinstance(node, ir.Constant): + what_to_print = f"Constant({node.constant_data})" else: base_name = node.__class__.__name__ diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py index b57bffea6..eb6376ec4 100644 --- a/hdk/common/mlir/converters.py +++ b/hdk/common/mlir/converters.py @@ -133,7 +133,7 @@ V0_OPSET_CONVERSION_FUNCTIONS = { ir.Add: add, ir.Sub: sub, ir.Mul: mul, - ir.ConstantInput: constant, + ir.Constant: constant, } # pylint: enable=no-name-in-module,no-member diff --git a/hdk/common/optimization/topological.py b/hdk/common/optimization/topological.py index 3cc249bc9..8b5457127 100644 --- a/hdk/common/optimization/topological.py +++ b/hdk/common/optimization/topological.py @@ -95,12 +95,12 @@ def convert_float_subgraph_to_fused_node( return None # Only one variable input node, find which node feeds its input - non_constant_input_nodes = [ - node for node in float_subgraph_start_nodes if not isinstance(node, ir.ConstantInput) + non_constant_start_nodes = [ + node for node in float_subgraph_start_nodes if not isinstance(node, ir.Constant) ] - assert len(non_constant_input_nodes) == 1 + assert len(non_constant_start_nodes) == 1 - current_subgraph_variable_input = non_constant_input_nodes[0] + current_subgraph_variable_input = non_constant_start_nodes[0] new_input_value = deepcopy(current_subgraph_variable_input.outputs[0]) nx_graph = op_graph.graph @@ -233,8 +233,8 @@ def subgraph_has_unique_variable_input( float_subgraph_start_nodes (Set[ir.IntermediateNode]): The nodes starting the subgraph. Returns: - bool: True if only one of the nodes is not an ir.ConstantInput + bool: True if only one of the nodes is not an ir.Constant """ # Only one input to the subgraph where computations are done in floats is variable, this # is the only case we can manage with ArbitraryFunction fusing - return sum(not isinstance(node, ir.ConstantInput) for node in float_subgraph_start_nodes) == 1 + return sum(not isinstance(node, ir.Constant) for node in float_subgraph_start_nodes) == 1 diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index fe3610e63..22f1b4f41 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -174,7 +174,7 @@ class Input(IntermediateNode): ) -class ConstantInput(IntermediateNode): +class Constant(IntermediateNode): """Node representing a constant of the program.""" _constant_data: Any @@ -199,14 +199,14 @@ class ConstantInput(IntermediateNode): def is_equivalent_to(self, other: object) -> bool: return ( - isinstance(other, ConstantInput) + isinstance(other, Constant) and self.constant_data == other.constant_data and super().is_equivalent_to(other) ) @property def constant_data(self) -> Any: - """Returns the constant_data stored in the ConstantInput node. + """Returns the constant_data stored in the Constant node. Returns: Any: The constant data that was stored. diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 91a4ac401..365ad702a 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -9,7 +9,7 @@ from numpy.typing import DTypeLike from ..common.data_types import BaseValue from ..common.data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype from ..common.operator_graph import OPGraph -from ..common.representation.intermediate import ArbitraryFunction, ConstantInput +from ..common.representation.intermediate import ArbitraryFunction, Constant from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from .np_dtypes_helpers import ( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES, @@ -22,8 +22,8 @@ SUPPORTED_TYPES_FOR_TRACING = (int, float, numpy.ndarray) + tuple( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES ) -NPConstantInput = partial( - ConstantInput, +NPConstant = partial( + Constant, get_base_value_for_data_func=get_base_value_for_numpy_or_python_constant_data, ) @@ -102,7 +102,7 @@ class NPTracer(BaseTracer): ) def _make_const_input_tracer(self, constant_data: Any) -> "NPTracer": - return self.__class__([], NPConstantInput(constant_data), 0) + return self.__class__([], NPConstant(constant_data), 0) @staticmethod def _manage_dtypes(ufunc: numpy.ufunc, *input_tracers: "NPTracer"): diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 656426dc9..64d669752 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -98,7 +98,7 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): ) ref_graph.add_node(intermediate_arbitrary_function, content=intermediate_arbitrary_function) - constant_3 = ir.ConstantInput(3) + constant_3 = ir.Constant(3) ref_graph.add_node(constant_3, content=constant_3) output_add = ir.Add((intermediate_arbitrary_function.outputs[0], constant_3.outputs[0])) diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 86c94db65..ae6629ae7 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -29,8 +29,8 @@ from hdk.common.representation import intermediate as ir id="Mul", ), pytest.param(ir.Input(ClearValue(Integer(32, True)), "in", 0), [42], 42, id="Input"), - pytest.param(ir.ConstantInput(42), None, 42, id="ConstantInput"), - pytest.param(ir.ConstantInput(-42), None, -42, id="ConstantInput"), + pytest.param(ir.Constant(42), None, 42, id="Constant"), + pytest.param(ir.Constant(-42), None, -42, id="Constant"), pytest.param( ir.ArbitraryFunction( EncryptedValue(Integer(7, False)), lambda x: x + 3, Integer(7, False) @@ -152,18 +152,18 @@ def test_evaluate( False, ), ( - ir.ConstantInput(10), - ir.ConstantInput(10), + ir.Constant(10), + ir.Constant(10), True, ), ( - ir.ConstantInput(10), + ir.Constant(10), ir.Input(EncryptedValue(Integer(8, False)), "x", 0), False, ), ( - ir.ConstantInput(10), - ir.ConstantInput(10.0), + ir.Constant(10), + ir.Constant(10.0), False, ), ( diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index b29c18cd7..a3965e2cd 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -47,22 +47,22 @@ def issue_130_c(x, y): "\n%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)\nreturn(%6)", ), - (lambda x, y: x + 1, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: 1 + x, "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: (-1) + x, "\n%0 = x\n%1 = ConstantInput(-1)\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: 3 * x, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)\nreturn(%2)"), - (lambda x, y: x * 3, "\n%0 = x\n%1 = ConstantInput(3)\n%2 = Mul(0, 1)\nreturn(%2)"), - (lambda x, y: x * (-3), "\n%0 = x\n%1 = ConstantInput(-3)\n%2 = Mul(0, 1)\nreturn(%2)"), - (lambda x, y: x - 11, "\n%0 = x\n%1 = ConstantInput(11)\n%2 = Sub(0, 1)\nreturn(%2)"), - (lambda x, y: 11 - x, "\n%0 = ConstantInput(11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), - (lambda x, y: (-11) - x, "\n%0 = ConstantInput(-11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: x + 1, "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: 1 + x, "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: (-1) + x, "\n%0 = x\n%1 = Constant(-1)\n%2 = Add(0, 1)\nreturn(%2)"), + (lambda x, y: 3 * x, "\n%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)"), + (lambda x, y: x * 3, "\n%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)"), + (lambda x, y: x * (-3), "\n%0 = x\n%1 = Constant(-3)\n%2 = Mul(0, 1)\nreturn(%2)"), + (lambda x, y: x - 11, "\n%0 = x\n%1 = Constant(11)\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: 11 - x, "\n%0 = Constant(11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: (-11) - x, "\n%0 = Constant(-11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), ( lambda x, y: x + 13 - y * (-21) * y + 44, - "\n%0 = ConstantInput(44)" + "\n%0 = Constant(44)" "\n%1 = x" - "\n%2 = ConstantInput(13)" + "\n%2 = Constant(13)" "\n%3 = y" - "\n%4 = ConstantInput(-21)" + "\n%4 = Constant(-21)" "\n%5 = Add(1, 2)" "\n%6 = Mul(3, 4)" "\n%7 = Mul(6, 3)" @@ -74,8 +74,8 @@ def issue_130_c(x, y): ( lambda x, y: (x + 1, x + y + 2), "\n%0 = x" - "\n%1 = ConstantInput(1)" - "\n%2 = ConstantInput(2)" + "\n%1 = Constant(1)" + "\n%2 = Constant(2)" "\n%3 = y" "\n%4 = Add(0, 1)" "\n%5 = Add(0, 3)" @@ -88,28 +88,28 @@ def issue_130_c(x, y): ), ( lambda x, y: (x, x + 1), - "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%0, %2)", + "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%0, %2)", ), ( lambda x, y: (x + 1, x + 1), "\n%0 = x" - "\n%1 = ConstantInput(1)" - "\n%2 = ConstantInput(1)" + "\n%1 = Constant(1)" + "\n%2 = Constant(1)" "\n%3 = Add(0, 1)" "\n%4 = Add(0, 2)" "\nreturn(%3, %4)", ), ( issue_130_a, - "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Add(0, 1)\nreturn(%2, %2)", + "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2, %2)", ), ( issue_130_b, - "\n%0 = x\n%1 = ConstantInput(1)\n%2 = Sub(0, 1)\nreturn(%2, %2)", + "\n%0 = x\n%1 = Constant(1)\n%2 = Sub(0, 1)\nreturn(%2, %2)", ), ( issue_130_c, - "\n%0 = ConstantInput(1)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2, %2)", + "\n%0 = Constant(1)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2, %2)", ), ], ) @@ -159,7 +159,7 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x\n%1 = ConstantInput(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)", + "\n%0 = x\n%1 = Constant(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)", ), ], ) @@ -236,7 +236,7 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedValue(Integer(2, is_signed=False))}, "\n%0 = x # Integer" - "\n%1 = ConstantInput(4) # Integer" + "\n%1 = Constant(4) # Integer" "\n%2 = Add(0, 1) # Integer" "\n%3 = TLU(2) # Integer" "\nreturn(%3)", @@ -245,7 +245,7 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[LOOKUP_TABLE_FROM_3B_TO_2B[x + 4]], {"x": EncryptedValue(Integer(2, is_signed=False))}, "\n%0 = x # Integer" - "\n%1 = ConstantInput(4) # Integer" + "\n%1 = Constant(4) # Integer" "\n%2 = Add(0, 1) # Integer" "\n%3 = TLU(2) # Integer" "\n%4 = TLU(3) # Integer" From 4b3fb772b8cfda8a57165ba1002213ca73c818e2 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 23 Aug 2021 17:00:34 +0300 Subject: [PATCH 0127/1104] chore: integrate checking whether notebooks are sanitized into the pcc target --- Makefile | 6 ++++- script/nbmake_utils/notebook_sanitize.py | 32 ++++++++++++++++-------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index eca4d4c49..f1ed317a9 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,10 @@ check_python_format: poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --dir benchmarks --check .PHONY: check_python_format +check_strip_nb: + poetry run python ./script/nbmake_utils/notebook_sanitize.py examples --check +.PHONY: strip_nb + pylint: poetry run pylint --rcfile=pylintrc hdk tests benchmarks .PHONY: pylint @@ -38,7 +42,7 @@ pcc: @$(MAKE) --keep-going --jobs $$(nproc) --output-sync --no-print-directory pcc_internal .PHONY: pcc -pcc_internal: check_python_format python_linting mypy_ci pydocstyle +pcc_internal: check_python_format check_strip_nb python_linting mypy_ci pydocstyle .PHONY: pcc_internal pytest: diff --git a/script/nbmake_utils/notebook_sanitize.py b/script/nbmake_utils/notebook_sanitize.py index b553f8d52..adcd4499e 100644 --- a/script/nbmake_utils/notebook_sanitize.py +++ b/script/nbmake_utils/notebook_sanitize.py @@ -1,21 +1,33 @@ +import argparse import json -import sys from pathlib import Path def main(): - path_to_glob = Path(sys.argv[1]) - notebooks = path_to_glob.glob("*.ipynb") + parser = argparse.ArgumentParser(description='Sanitizer for Jupyter Notebooks') - for notebook_file in notebooks: - with open(notebook_file, "r") as f: - notebook_dict = json.load(f) - notebook_dict["metadata"] = {} + parser.add_argument('base', type=str, help='directory which contains the notebooks') + parser.add_argument('--check', action='store_true', help='flag to enable just checking mode') - with open(notebook_file, "w", newline="\n") as f: - json.dump(notebook_dict, f, indent=1, ensure_ascii=False) - f.write("\n") + args = parser.parse_args() + + base = Path(args.base) + notebooks = base.glob("*.ipynb") + + for notebook in notebooks: + with open(notebook, "r") as f: + content = json.load(f) + + if args.check: + if len(content["metadata"]) != 0: + print("Notebooks are not sanitized. Please run `make conformance`.") + exit(1) + else: + content["metadata"] = {} + with open(notebook, "w", newline="\n") as f: + json.dump(content, f, indent=1, ensure_ascii=False) + f.write("\n") if __name__ == "__main__": From 1d5be5a1e773d9ed06e031ab845d54b2c65c93f3 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 24 Aug 2021 12:18:32 +0300 Subject: [PATCH 0128/1104] refactor: re-organize data types and values --- benchmarks/test_compilation_and_evaluation.py | 2 +- examples/QuantizedLinearRegression.ipynb | 2 +- examples/QuantizedLogisticRegression.ipynb | 2 +- hdk/common/data_types/__init__.py | 5 +- hdk/common/data_types/dtypes_helpers.py | 2 +- hdk/common/mlir/mlir_converter.py | 4 +- hdk/common/representation/intermediate.py | 2 +- hdk/common/tracing/base_tracer.py | 2 +- hdk/common/tracing/tracing_helpers.py | 2 +- hdk/common/values/__init__.py | 5 ++ hdk/common/values/base.py | 43 ++++++++++ hdk/common/values/scalars.py | 39 +++++++++ .../values.py => values/tensors.py} | 83 +------------------ hdk/hnumpy/compile.py | 2 +- hdk/hnumpy/np_dtypes_helpers.py | 2 +- hdk/hnumpy/tracing.py | 2 +- .../bounds_measurement/test_dataset_eval.py | 2 +- tests/common/compilation/test_artifacts.py | 2 +- .../common/compilation/test_configuration.py | 2 +- .../common/data_types/test_dtypes_helpers.py | 2 +- tests/common/data_types/test_values.py | 2 +- tests/common/extensions/test_table.py | 2 +- tests/common/mlir/test_converters.py | 2 +- tests/common/mlir/test_mlir_converter.py | 2 +- .../common/optimization/test_float_fusing.py | 2 +- .../representation/test_intermediate.py | 2 +- tests/common/test_common_helpers.py | 2 +- tests/hnumpy/test_compile.py | 2 +- tests/hnumpy/test_debugging.py | 2 +- tests/hnumpy/test_tracing.py | 2 +- 30 files changed, 118 insertions(+), 109 deletions(-) create mode 100644 hdk/common/values/__init__.py create mode 100644 hdk/common/values/base.py create mode 100644 hdk/common/values/scalars.py rename hdk/common/{data_types/values.py => values/tensors.py} (53%) diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py index a7e45d6b3..587c68cc7 100644 --- a/benchmarks/test_compilation_and_evaluation.py +++ b/benchmarks/test_compilation_and_evaluation.py @@ -5,7 +5,7 @@ import itertools import pytest from hdk.common.data_types.integers import SignedInteger, UnsignedInteger -from hdk.common.data_types.values import EncryptedValue +from hdk.common.values import EncryptedValue from hdk.hnumpy.compile import compile_numpy_function_into_op_graph diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 56db26114..f5e4cf690 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -622,7 +622,7 @@ "outputs": [], "source": [ "from hdk.common.data_types.integers import Integer\n", - "from hdk.common.data_types.values import EncryptedValue\n", + "from hdk.common.values import EncryptedValue\n", "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 451a7396f..ccf05f67b 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -726,7 +726,7 @@ "outputs": [], "source": [ "from hdk.common.data_types.integers import Integer\n", - "from hdk.common.data_types.values import EncryptedValue\n", + "from hdk.common.values import EncryptedValue\n", "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index a89d9a3fb..758381e62 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,4 +1,3 @@ """Module for data types code and data structures.""" -from . import dtypes_helpers, integers, values -from .integers import Integer -from .values import BaseValue +from . import dtypes_helpers, integers +from .integers import Integer, SignedInteger, UnsignedInteger diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 91db9f1f9..06516dfcd 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -4,10 +4,10 @@ from copy import deepcopy from functools import partial from typing import Callable, Union, cast +from ..values import BaseValue, ClearValue, EncryptedValue, ScalarValue from .base import BaseDataType from .floats import Float from .integers import Integer, get_bits_to_represent_value_as_integer -from .values import BaseValue, ClearValue, EncryptedValue, ScalarValue INTEGER_TYPES = (Integer,) FLOAT_TYPES = (Float,) diff --git a/hdk/common/mlir/mlir_converter.py b/hdk/common/mlir/mlir_converter.py index d1f0d8668..b48d1ca78 100644 --- a/hdk/common/mlir/mlir_converter.py +++ b/hdk/common/mlir/mlir_converter.py @@ -9,7 +9,7 @@ from mlir.ir import Context, InsertionPoint, IntegerType, Location, Module from mlir.ir import Type as MLIRType from zamalang.dialects import hlfhe -from .. import data_types +from .. import values from ..data_types import Integer from ..data_types.dtypes_helpers import ( value_is_clear_integer, @@ -41,7 +41,7 @@ class MLIRConverter: self.context = Context() zamalang.register_dialects(self.context) - def hdk_value_to_mlir_type(self, value: data_types.BaseValue) -> MLIRType: + def hdk_value_to_mlir_type(self, value: values.BaseValue) -> MLIRType: """Convert an HDK value to its corresponding MLIR Type. Args: diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 22f1b4f41..79a6b9690 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -4,12 +4,12 @@ from abc import ABC, abstractmethod from copy import deepcopy from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple -from ..data_types import BaseValue from ..data_types.base import BaseDataType from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, mix_scalar_values_determine_holding_dtype, ) +from ..values import BaseValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 2c9018e54..689d88f9a 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -3,9 +3,9 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Iterable, List, Tuple, Type, Union -from ..data_types import BaseValue from ..representation import intermediate as ir from ..representation.intermediate import IR_MIX_VALUES_FUNC_ARG_NAME +from ..values import BaseValue class BaseTracer(ABC): diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py index f60a33e72..0e643a894 100644 --- a/hdk/common/tracing/tracing_helpers.py +++ b/hdk/common/tracing/tracing_helpers.py @@ -6,8 +6,8 @@ from typing import Callable, Dict, Iterable, OrderedDict, Set, Type import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph -from ..data_types import BaseValue from ..representation import intermediate as ir +from ..values import BaseValue from .base_tracer import BaseTracer diff --git a/hdk/common/values/__init__.py b/hdk/common/values/__init__.py new file mode 100644 index 000000000..944df24c4 --- /dev/null +++ b/hdk/common/values/__init__.py @@ -0,0 +1,5 @@ +"""Module for value structures.""" + +from .base import BaseValue +from .scalars import ClearValue, EncryptedValue, ScalarValue +from .tensors import ClearTensor, EncryptedTensor, TensorValue diff --git a/hdk/common/values/base.py b/hdk/common/values/base.py new file mode 100644 index 000000000..c4ffd16dd --- /dev/null +++ b/hdk/common/values/base.py @@ -0,0 +1,43 @@ +"""Module that defines the values in a program.""" + +from abc import ABC, abstractmethod +from copy import deepcopy + +from ..data_types.base import BaseDataType + + +class BaseValue(ABC): + """Abstract base class to represent any kind of value in a program.""" + + data_type: BaseDataType + _is_encrypted: bool + + def __init__(self, data_type: BaseDataType, is_encrypted: bool) -> None: + self.data_type = deepcopy(data_type) + self._is_encrypted = is_encrypted + + def __repr__(self) -> str: # pragma: no cover + encrypted_str = "Encrypted" if self._is_encrypted else "Clear" + return f"{encrypted_str}{self.__class__.__name__}<{self.data_type!r}>" + + @abstractmethod + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) and self.data_type == other.data_type + + @property + def is_encrypted(self) -> bool: + """Whether Value is encrypted or not. + + Returns: + bool: True if encrypted False otherwise + """ + return self._is_encrypted + + @property + def is_clear(self) -> bool: + """Whether Value is clear or not. + + Returns: + bool: True if clear False otherwise + """ + return not self._is_encrypted diff --git a/hdk/common/values/scalars.py b/hdk/common/values/scalars.py new file mode 100644 index 000000000..8ae8efcbd --- /dev/null +++ b/hdk/common/values/scalars.py @@ -0,0 +1,39 @@ +"""Module that defines the scalar values in a program.""" + +from ..data_types.base import BaseDataType +from .base import BaseValue + + +class ScalarValue(BaseValue): + """Class representing a scalar value.""" + + def __eq__(self, other: object) -> bool: + return BaseValue.__eq__(self, other) + + +def make_clear_scalar(data_type: BaseDataType) -> ScalarValue: + """Helper to create a clear ScalarValue. + + Args: + data_type (BaseDataType): The data type for the value. + + Returns: + ScalarValue: The corresponding ScalarValue. + """ + return ScalarValue(data_type=data_type, is_encrypted=False) + + +def make_encrypted_scalar(data_type: BaseDataType) -> ScalarValue: + """Helper to create an encrypted ScalarValue. + + Args: + data_type (BaseDataType): The data type for the value. + + Returns: + ScalarValue: The corresponding ScalarValue. + """ + return ScalarValue(data_type=data_type, is_encrypted=True) + + +ClearValue = make_clear_scalar +EncryptedValue = make_encrypted_scalar diff --git a/hdk/common/data_types/values.py b/hdk/common/values/tensors.py similarity index 53% rename from hdk/common/data_types/values.py rename to hdk/common/values/tensors.py index 16c7f409b..1f4b9effa 100644 --- a/hdk/common/data_types/values.py +++ b/hdk/common/values/tensors.py @@ -1,87 +1,10 @@ -"""File holding classes representing values used by an FHE program.""" +"""Module that defines the tensor values in a program.""" -from abc import ABC, abstractmethod -from copy import deepcopy from math import prod from typing import Optional, Tuple -from .base import BaseDataType - - -class BaseValue(ABC): - """Abstract base class to represent any kind of value in a program.""" - - data_type: BaseDataType - _is_encrypted: bool - - def __init__(self, data_type: BaseDataType, is_encrypted: bool) -> None: - self.data_type = deepcopy(data_type) - self._is_encrypted = is_encrypted - - def __repr__(self) -> str: # pragma: no cover - encrypted_str = "Encrypted" if self._is_encrypted else "Clear" - return f"{encrypted_str}{self.__class__.__name__}<{self.data_type!r}>" - - @abstractmethod - def __eq__(self, other: object) -> bool: - return isinstance(other, self.__class__) and self.data_type == other.data_type - - @property - def is_encrypted(self) -> bool: - """Whether Value is encrypted or not. - - Returns: - bool: True if encrypted False otherwise - """ - return self._is_encrypted - - @property - def is_clear(self) -> bool: - """Whether Value is clear or not. - - Returns: - bool: True if clear False otherwise - """ - return not self._is_encrypted - - -class ScalarValue(BaseValue): - """Class representing a scalar value.""" - - def __eq__(self, other: object) -> bool: - return BaseValue.__eq__(self, other) - - -def make_clear_scalar( - data_type: BaseDataType, -) -> ScalarValue: - """Helper to create a clear ScalarValue. - - Args: - data_type (BaseDataType): The data type for the value. - - Returns: - ScalarValue: The corresponding ScalarValue. - """ - return ScalarValue(data_type=data_type, is_encrypted=False) - - -def make_encrypted_scalar( - data_type: BaseDataType, -) -> ScalarValue: - """Helper to create an encrypted ScalarValue. - - Args: - data_type (BaseDataType): The data type for the value. - - Returns: - ScalarValue: The corresponding ScalarValue. - """ - return ScalarValue(data_type=data_type, is_encrypted=True) - - -ClearValue = make_clear_scalar -EncryptedValue = make_encrypted_scalar +from ..data_types.base import BaseDataType +from .base import BaseValue class TensorValue(BaseValue): diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 04f2fc4e1..a0326b6a9 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -7,7 +7,6 @@ from zamalang import CompilerEngine from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts, CompilationConfiguration -from ..common.data_types import BaseValue from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( is_graph_values_compatible_with_mlir, @@ -16,6 +15,7 @@ from ..common.mlir.utils import ( from ..common.operator_graph import OPGraph from ..common.optimization.topological import fuse_float_operations from ..common.representation import intermediate as ir +from ..common.values import BaseValue from ..hnumpy.tracing import trace_numpy_function from .np_dtypes_helpers import get_base_data_type_for_numpy_or_python_constant_data diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 82d1ebb00..0cf1792ae 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -15,7 +15,7 @@ from ..common.data_types.dtypes_helpers import ( ) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer -from ..common.data_types.values import BaseValue, ScalarValue +from ..common.values import BaseValue, ScalarValue NUMPY_TO_HDK_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.int32): Integer(32, is_signed=True), diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 365ad702a..26eadd548 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -6,11 +6,11 @@ from typing import Any, Callable, Dict import numpy from numpy.typing import DTypeLike -from ..common.data_types import BaseValue from ..common.data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype from ..common.operator_graph import OPGraph from ..common.representation.intermediate import ArbitraryFunction, Constant from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters +from ..common.values import BaseValue from .np_dtypes_helpers import ( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES, convert_numpy_dtype_to_base_data_type, diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index 98f5b2b43..210b138e5 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -7,7 +7,7 @@ import pytest from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue +from hdk.common.values import EncryptedValue from hdk.hnumpy.tracing import trace_numpy_function diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index d36918afd..d3c326f35 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -5,7 +5,7 @@ from pathlib import Path from hdk.common.compilation import CompilationArtifacts from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue +from hdk.common.values import EncryptedValue from hdk.hnumpy.compile import compile_numpy_function_into_op_graph diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index 0280fe2a7..890c22f11 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -7,7 +7,7 @@ import pytest from hdk.common.compilation import CompilationConfiguration from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue +from hdk.common.values import EncryptedValue from hdk.hnumpy.compile import compile_numpy_function_into_op_graph diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 805424c99..5df0666f8 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -11,7 +11,7 @@ from hdk.common.data_types.dtypes_helpers import ( ) from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import BaseValue, ClearValue, EncryptedValue +from hdk.common.values import BaseValue, ClearValue, EncryptedValue @pytest.mark.parametrize( diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py index 3f84a34a6..7359a3760 100644 --- a/tests/common/data_types/test_values.py +++ b/tests/common/data_types/test_values.py @@ -9,7 +9,7 @@ import pytest from hdk.common.data_types.base import BaseDataType from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearTensor, EncryptedTensor, TensorValue +from hdk.common.values import ClearTensor, EncryptedTensor, TensorValue class DummyDtype(BaseDataType): diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 64d669752..bc5658d7b 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -7,9 +7,9 @@ import pytest from hdk.common import is_a_power_of_2 from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue from hdk.common.extensions.table import LookupTable from hdk.common.representation import intermediate as ir +from hdk.common.values import EncryptedValue from hdk.hnumpy import tracing diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index 894d42a77..6c2ef33ca 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -3,8 +3,8 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearValue from hdk.common.mlir.converters import add, constant, mul, sub +from hdk.common.values import ClearValue class MockNode: diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 617dd1c52..5a370d016 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -8,8 +8,8 @@ from zamalang import compiler from zamalang.dialects import hlfhe from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter +from hdk.common.values import ClearValue, EncryptedValue from hdk.hnumpy.compile import compile_numpy_function_into_op_graph diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 046cfdbdb..9f1adccfb 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -6,8 +6,8 @@ import numpy import pytest from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue from hdk.common.optimization.topological import fuse_float_operations +from hdk.common.values import EncryptedValue from hdk.hnumpy.tracing import trace_numpy_function diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index ae6629ae7..27f6618c4 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -3,8 +3,8 @@ import pytest from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.representation import intermediate as ir +from hdk.common.values import ClearValue, EncryptedValue @pytest.mark.parametrize( diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index a0e076a45..f2cf90a81 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -7,7 +7,7 @@ import pytest from hdk.common import check_op_graph_is_integer_program, is_a_power_of_2 from hdk.common.data_types.floats import Float64 from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue +from hdk.common.values import EncryptedValue from hdk.hnumpy.tracing import trace_numpy_function diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 8565b7334..67f93dc3a 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -6,9 +6,9 @@ import numpy import pytest from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import EncryptedValue from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable +from hdk.common.values import EncryptedValue from hdk.hnumpy.compile import ( compile_numpy_function, compile_numpy_function_into_op_graph, diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index a3965e2cd..78aad1c31 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -3,9 +3,9 @@ import pytest from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable +from hdk.common.values import ClearValue, EncryptedValue from hdk.hnumpy import tracing LOOKUP_TABLE_FROM_2B_TO_4B = LookupTable([9, 2, 4, 11]) diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 8d7e25ded..3405d13bc 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -6,8 +6,8 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.data_types.values import ClearValue, EncryptedValue from hdk.common.representation import intermediate as ir +from hdk.common.values import ClearValue, EncryptedValue from hdk.hnumpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] From 30e00df97749e8e4b0ff3b3bc486804292b49cf0 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 23 Aug 2021 15:09:33 +0200 Subject: [PATCH 0129/1104] chore(tools): update pylint target to lint dirs separately - avoids triggering duplicate code detection between hdk tests and benchmarks - update helper script serialize_targets.sh to avoid printing dir --- Makefile | 16 +++++++++++++++- script/make_utils/serialize_targets.sh | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f1ed317a9..8046b9cf7 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,23 @@ check_strip_nb: .PHONY: strip_nb pylint: - poetry run pylint --rcfile=pylintrc hdk tests benchmarks + +poetry run env bash script/make_utils/serialize_targets.sh pylint_src pylint_tests pylint_benchmarks .PHONY: pylint +pylint_src: + poetry run pylint --rcfile=pylintrc hdk +.PHONY: pylint_src + +pylint_tests: + @# Disable duplicate code detection in tests + poetry run pylint --disable=R0801 --rcfile=pylintrc tests +.PHONY: pylint_tests + +pylint_benchmarks: + @# Disable duplicate code detection in benchmarks + poetry run pylint --disable=R0801 --rcfile=pylintrc benchmarks +.PHONY: pylint_benchmarks + flake8: poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" hdk/ tests/ benchmarks/ .PHONY: flake8 diff --git a/script/make_utils/serialize_targets.sh b/script/make_utils/serialize_targets.sh index 882bdb6e6..2b4b802af 100755 --- a/script/make_utils/serialize_targets.sh +++ b/script/make_utils/serialize_targets.sh @@ -5,7 +5,7 @@ set +e EXIT_CODE=0 for make_target in "$@"; do - make "${make_target}" + make --no-print-directory "${make_target}" if [[ "$?" != "0" ]]; then EXIT_CODE=1 fi From cc1221eac5614271e0488007420bba4585273022 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 20 Aug 2021 16:19:06 +0200 Subject: [PATCH 0130/1104] dev(bounds): add custom min and max func callbacks to manage foreign types - allows us to pass a specialized function to estimate min and max for e.g. numpy and/or torch tensors --- hdk/common/bounds_measurement/dataset_eval.py | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/hdk/common/bounds_measurement/dataset_eval.py index d3684ce6c..b47889f98 100644 --- a/hdk/common/bounds_measurement/dataset_eval.py +++ b/hdk/common/bounds_measurement/dataset_eval.py @@ -1,11 +1,17 @@ """Code to evaluate the IR graph on datasets.""" -from typing import Any, Iterator, Tuple +from typing import Any, Callable, Dict, Iterator, Tuple from ..operator_graph import OPGraph +from ..representation.intermediate import IntermediateNode -def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, dataset: Iterator[Tuple[Any, ...]]): +def eval_op_graph_bounds_on_dataset( + op_graph: OPGraph, + dataset: Iterator[Tuple[Any, ...]], + min_func: Callable[[Any, Any], Any] = min, + max_func: Callable[[Any, Any], Any] = max, +) -> Dict[IntermediateNode, Dict[str, Any]]: """Evaluate the bounds with a dataset. Evaluate the bounds for all output values of the operators in the graph op_graph over data @@ -16,39 +22,45 @@ def eval_op_graph_bounds_on_dataset(op_graph: OPGraph, dataset: Iterator[Tuple[A dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters + min_func (Callable[[Any, Any], Any], optional): custom function to compute a scalar minimum + between two values that can be encountered during evaluation (for e.g. numpy or torch + tensors). Defaults to min. + max_func (Callable[[Any, Any], Any], optional): custom function to compute a scalar maximum + between two values that can be encountered during evaluation (for e.g. numpy or torch + tensors). Defaults to max. Returns: - Dict: dict containing the bounds for each node from op_graph, stored with the node as key - and a dict with keys "min" and "max" as value + Dict[IntermediateNode, Dict[str, Any]]: dict containing the bounds for each node from + op_graph, stored with the node as key and a dict with keys "min" and "max" as value. """ - def check_dataset_input_is_valid(data_to_check): + def check_dataset_input_len_is_valid(data_to_check): assert len(data_to_check) == len(op_graph.input_nodes), ( f"Got input data from dataset of len: {len(data_to_check)}, " f"function being evaluated has {len(op_graph.input_nodes)} inputs, please make " f"sure your data generator returns valid tuples of input values" ) - # TODO: change this to be more generic and check coherence between the input data type and - # the corresponding Input ir node expected data type - assert all( - isinstance(val, int) for val in data_to_check - ), "For now dataset evaluation only support int as inputs, please check your dataset" + + # TODO: do we want to check coherence between the input data type and the corresponding Input ir + # node expected data type ? Not considering bit_width as they may not make sense at this stage first_input_data = dict(enumerate(next(dataset))) - check_dataset_input_is_valid(first_input_data.values()) + check_dataset_input_len_is_valid(first_input_data.values()) first_output = op_graph.evaluate(first_input_data) + # We evaluate the min and max func to be able to resolve the tensors min and max rather than + # having the tensor itself as the stored min and max values. node_bounds = { - node: {"min": first_output[node], "max": first_output[node]} - for node in op_graph.graph.nodes() + node: {"min": min_func(value, value), "max": max_func(value, value)} + for node, value in first_output.items() } for input_data in dataset: current_input_data = dict(enumerate(input_data)) - check_dataset_input_is_valid(current_input_data.values()) + check_dataset_input_len_is_valid(current_input_data.values()) current_output = op_graph.evaluate(current_input_data) for node, value in current_output.items(): - node_bounds[node]["min"] = min(node_bounds[node]["min"], value) - node_bounds[node]["max"] = max(node_bounds[node]["max"], value) + node_bounds[node]["min"] = min_func(node_bounds[node]["min"], value) + node_bounds[node]["max"] = max_func(node_bounds[node]["max"], value) return node_bounds From fc3ae6461c1d65eb403611b0b7bda7b6f430dab2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 23 Aug 2021 14:41:31 +0200 Subject: [PATCH 0131/1104] refacto: change the way mixing values is handled - use an intermediate function that checks which mix function to use - update function used in hnumpy tracing --- hdk/common/data_types/dtypes_helpers.py | 94 +++++++++++++++++-- hdk/hnumpy/tracing.py | 4 +- .../common/data_types/test_dtypes_helpers.py | 76 ++++++++++++++- 3 files changed, 161 insertions(+), 13 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 06516dfcd..06a23431e 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -4,7 +4,15 @@ from copy import deepcopy from functools import partial from typing import Callable, Union, cast -from ..values import BaseValue, ClearValue, EncryptedValue, ScalarValue +from ..values import ( + BaseValue, + ClearTensor, + ClearValue, + EncryptedTensor, + EncryptedValue, + ScalarValue, + TensorValue, +) from .base import BaseDataType from .floats import Float from .integers import Integer, get_bits_to_represent_value_as_integer @@ -134,19 +142,22 @@ def find_type_to_hold_both_lossy( return type_to_return -def mix_scalar_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> ScalarValue: - """Return mixed value with data type able to hold both value1 and value2 dtypes. +def mix_scalar_values_determine_holding_dtype( + value1: ScalarValue, + value2: ScalarValue, +) -> ScalarValue: + """Return mixed ScalarValue with data type able to hold both value1 and value2 dtypes. Returns a ScalarValue that would result from computation on both value1 and value2 while determining the data type able to hold both value1 and value2 data type (this can be lossy with floats). Args: - value1 (BaseValue): first ScalarValue to mix. - value2 (BaseValue): second ScalarValue to mix. + value1 (ScalarValue): first ScalarValue to mix. + value2 (ScalarValue): second ScalarValue to mix. Returns: - ScalarValue: The resulting mixed BaseValue with data type able to hold both value1 and + ScalarValue: The resulting mixed ScalarValue with data type able to hold both value1 and value2 dtypes. """ @@ -164,6 +175,77 @@ def mix_scalar_values_determine_holding_dtype(value1: BaseValue, value2: BaseVal return mixed_value +def mix_tensor_values_determine_holding_dtype( + value1: TensorValue, + value2: TensorValue, +) -> TensorValue: + """Return mixed TensorValue with data type able to hold both value1 and value2 dtypes. + + Returns a TensorValue that would result from computation on both value1 and value2 while + determining the data type able to hold both value1 and value2 data type (this can be lossy + with floats). + + Args: + value1 (TensorValue): first TensorValue to mix. + value2 (TensorValue): second TensorValue to mix. + + Returns: + TensorValue: The resulting mixed TensorValue with data type able to hold both value1 and + value2 dtypes. + """ + + assert isinstance(value1, TensorValue), f"Unsupported value1: {value1}, expected TensorValue" + assert isinstance(value2, TensorValue), f"Unsupported value2: {value2}, expected TensorValue" + + assert value1.shape == value2.shape, ( + f"Tensors have different shapes which is not supported.\n" + f"value1: {value1.shape}, value2: {value2.shape}" + ) + + holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) + shape = value1.shape + + if value1.is_encrypted or value2.is_encrypted: + mixed_value = EncryptedTensor(data_type=holding_type, shape=shape) + else: + mixed_value = ClearTensor(data_type=holding_type, shape=shape) + + return mixed_value + + +def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> BaseValue: + """Return mixed BaseValue with data type able to hold both value1 and value2 dtypes. + + Returns a BaseValue that would result from computation on both value1 and value2 while + determining the data type able to hold both value1 and value2 data type (this can be lossy + with floats). Supports only mixing instances from the same class. + + Args: + value1 (BaseValue): first BaseValue to mix. + value2 (BaseValue): second BaseValue to mix. + + Raises: + ValueError: raised if the BaseValue is not one of (ScalarValue, TensorValue) + + Returns: + BaseValue: The resulting mixed BaseValue with data type able to hold both value1 and value2 + dtypes. + """ + + assert ( + value1.__class__ == value2.__class__ + ), f"Cannot mix values of different types: value 1:{type(value1)}, value2: {type(value2)}" + + if isinstance(value1, ScalarValue) and isinstance(value2, ScalarValue): + return mix_scalar_values_determine_holding_dtype(value1, value2) + if isinstance(value1, TensorValue) and isinstance(value2, TensorValue): + return mix_tensor_values_determine_holding_dtype(value1, value2) + + raise ValueError( + f"{mix_values_determine_holding_dtype.__name__} does not support value {type(value1)}" + ) + + def get_base_data_type_for_python_constant_data(constant_data: Union[int, float]) -> BaseDataType: """Helper function to determine the BaseDataType to hold the input constant data. diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 26eadd548..6b4e40e18 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -6,7 +6,7 @@ from typing import Any, Callable, Dict import numpy from numpy.typing import DTypeLike -from ..common.data_types.dtypes_helpers import mix_scalar_values_determine_holding_dtype +from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype from ..common.operator_graph import OPGraph from ..common.representation.intermediate import ArbitraryFunction, Constant from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters @@ -31,7 +31,7 @@ NPConstant = partial( class NPTracer(BaseTracer): """Tracer class for numpy operations.""" - _mix_values_func: Callable[..., BaseValue] = mix_scalar_values_determine_holding_dtype + _mix_values_func: Callable[..., BaseValue] = mix_values_determine_holding_dtype def __array_ufunc__(self, ufunc, method, *input_tracers, **kwargs): """Catch calls to numpy ufunc and routes them to tracing functions if supported. diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 5df0666f8..05a28bddb 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -5,13 +5,19 @@ import pytest from hdk.common.data_types.base import BaseDataType from hdk.common.data_types.dtypes_helpers import ( find_type_to_hold_both_lossy, - mix_scalar_values_determine_holding_dtype, + mix_values_determine_holding_dtype, value_is_encrypted_integer, value_is_encrypted_unsigned_integer, ) from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.values import BaseValue, ClearValue, EncryptedValue +from hdk.common.values import ( + BaseValue, + ClearTensor, + ClearValue, + EncryptedTensor, + EncryptedValue, +) @pytest.mark.parametrize( @@ -167,7 +173,67 @@ def test_mix_data_types( ), ], ) -def test_mix_values(value1: BaseValue, value2: BaseValue, expected_mixed_value: BaseValue): - """Test mix_values helper""" +def test_mix_scalar_values(value1, value2, expected_mixed_value): + """Test mix_values_determine_holding_dtype helper with scalars""" - assert expected_mixed_value == mix_scalar_values_determine_holding_dtype(value1, value2) + assert expected_mixed_value == mix_values_determine_holding_dtype(value1, value2) + + +@pytest.mark.parametrize( + "value1,value2,expected_mixed_value", + [ + pytest.param( + EncryptedTensor(Integer(7, False), (1, 2, 3)), + EncryptedTensor(Integer(7, False), (1, 2, 3)), + EncryptedTensor(Integer(7, False), (1, 2, 3)), + ), + pytest.param( + ClearTensor(Integer(7, False), (1, 2, 3)), + EncryptedTensor(Integer(7, False), (1, 2, 3)), + EncryptedTensor(Integer(7, False), (1, 2, 3)), + ), + pytest.param( + ClearTensor(Integer(7, False), (1, 2, 3)), + ClearTensor(Integer(7, False), (1, 2, 3)), + ClearTensor(Integer(7, False), (1, 2, 3)), + ), + pytest.param( + ClearTensor(Integer(7, False), (1, 2, 3)), + ClearTensor(Integer(7, False), (1, 2, 3)), + ClearTensor(Integer(7, False), (1, 2, 3)), + ), + pytest.param( + ClearTensor(Integer(7, False), (1, 2, 3)), + EncryptedValue(Integer(7, False)), + None, + marks=pytest.mark.xfail(raises=AssertionError), + ), + pytest.param( + ClearTensor(Integer(7, False), (1, 2, 3)), + ClearTensor(Integer(7, False), (3, 2, 1)), + None, + marks=pytest.mark.xfail(raises=AssertionError), + ), + ], +) +def test_mix_tensor_values(value1, value2, expected_mixed_value): + """Test mix_values_determine_holding_dtype helper with tensors""" + + assert expected_mixed_value == mix_values_determine_holding_dtype(value1, value2) + + +class DummyValue(BaseValue): + """DummyValue""" + + def __eq__(self, other: object) -> bool: + return BaseValue.__eq__(self, other) + + +def test_fail_mix_values_determine_holding_dtype(): + """Test function for failure case of mix_values_determine_holding_dtype""" + + with pytest.raises(ValueError, match=r".* does not support value .*"): + mix_values_determine_holding_dtype( + DummyValue(Integer(32, True), True), + DummyValue(Integer(32, True), True), + ) From 96b04b45e12455c49cef1638048f84096b8680a5 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 24 Aug 2021 12:08:12 +0200 Subject: [PATCH 0132/1104] refacto: rename check functions for values - indicate the checks are for ScalarValues as we now have TensorValues coming --- hdk/common/data_types/dtypes_helpers.py | 33 ++++++++++--------- hdk/common/mlir/converters.py | 22 ++++++------- hdk/common/mlir/mlir_converter.py | 8 ++--- hdk/common/mlir/utils.py | 16 ++++----- .../common/data_types/test_dtypes_helpers.py | 8 ++--- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 06a23431e..41fb98e80 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -22,63 +22,64 @@ FLOAT_TYPES = (Float,) BASE_DATA_TYPES = INTEGER_TYPES + FLOAT_TYPES -def value_is_encrypted_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted_integer. +def value_is_encrypted_scalar_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is an encrypted ScalarValue of type Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is an encrypted value of type Integer + bool: True if the passed value_to_check is an encrypted ScalarValue of type Integer """ return ( - isinstance(value_to_check, BaseValue) + isinstance(value_to_check, ScalarValue) and value_to_check.is_encrypted and isinstance(value_to_check.data_type, INTEGER_TYPES) ) -def value_is_encrypted_unsigned_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted_integer. +def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is an encrypted ScalarValue of type unsigned Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is an encrypted value of type Integer and unsigned + bool: True if the passed value_to_check is an encrypted ScalarValue of type Integer and + unsigned """ return ( - value_is_encrypted_integer(value_to_check) + value_is_encrypted_scalar_integer(value_to_check) and not cast(Integer, value_to_check.data_type).is_signed ) -def value_is_clear_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is a clear integer. +def value_is_clear_scalar_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is a clear ScalarValue of type Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is a clear value of type Integer + bool: True if the passed value_to_check is a clear ScalarValue of type Integer """ return ( - isinstance(value_to_check, BaseValue) + isinstance(value_to_check, ScalarValue) and value_to_check.is_clear and isinstance(value_to_check.data_type, INTEGER_TYPES) ) -def value_is_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is of type integer. +def value_is_scalar_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is a ScalarValue of type Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is a value of type Integer + bool: True if the passed value_to_check is a ScalarValue of type Integer """ - return isinstance(value_to_check, BaseValue) and isinstance( + return isinstance(value_to_check, ScalarValue) and isinstance( value_to_check.data_type, INTEGER_TYPES ) diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py index eb6376ec4..520b2a0d4 100644 --- a/hdk/common/mlir/converters.py +++ b/hdk/common/mlir/converters.py @@ -15,8 +15,8 @@ from zamalang.dialects import hlfhe from ...common.data_types.integers import Integer from ..data_types.dtypes_helpers import ( - value_is_clear_integer, - value_is_encrypted_unsigned_integer, + value_is_clear_scalar_integer, + value_is_encrypted_scalar_unsigned_integer, ) from ..representation import intermediate as ir @@ -25,18 +25,18 @@ def add(node, preds, ir_to_mlir_node, ctx): """Converter function for the addition intermediate node.""" assert len(node.inputs) == 2, "addition should have two inputs" assert len(node.outputs) == 1, "addition should have a single output" - if value_is_encrypted_unsigned_integer(node.inputs[0]) and value_is_clear_integer( + if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): return _add_eint_int(node, preds, ir_to_mlir_node, ctx) - if value_is_encrypted_unsigned_integer(node.inputs[1]) and value_is_clear_integer( + if value_is_encrypted_scalar_unsigned_integer(node.inputs[1]) and value_is_clear_scalar_integer( node.inputs[0] ): # flip lhs and rhs return _add_eint_int(node, preds[::-1], ir_to_mlir_node, ctx) - if value_is_encrypted_unsigned_integer(node.inputs[0]) and value_is_encrypted_unsigned_integer( - node.inputs[1] - ): + if value_is_encrypted_scalar_unsigned_integer( + node.inputs[0] + ) and value_is_encrypted_scalar_unsigned_integer(node.inputs[1]): return _add_eint_eint(node, preds, ir_to_mlir_node, ctx) raise TypeError( f"Don't support addition between {type(node.inputs[0])} and {type(node.inputs[1])}" @@ -69,7 +69,7 @@ def sub(node, preds, ir_to_mlir_node, ctx): """Converter function for the subtraction intermediate node.""" assert len(node.inputs) == 2, "subtraction should have two inputs" assert len(node.outputs) == 1, "subtraction should have a single output" - if value_is_clear_integer(node.inputs[0]) and value_is_encrypted_unsigned_integer( + if value_is_clear_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_unsigned_integer( node.inputs[1] ): return _sub_int_eint(node, preds, ir_to_mlir_node, ctx) @@ -93,11 +93,11 @@ def mul(node, preds, ir_to_mlir_node, ctx): """Converter function for the multiplication intermediate node.""" assert len(node.inputs) == 2, "multiplication should have two inputs" assert len(node.outputs) == 1, "multiplication should have a single output" - if value_is_encrypted_unsigned_integer(node.inputs[0]) and value_is_clear_integer( + if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): return _mul_eint_int(node, preds, ir_to_mlir_node, ctx) - if value_is_encrypted_unsigned_integer(node.inputs[1]) and value_is_clear_integer( + if value_is_encrypted_scalar_unsigned_integer(node.inputs[1]) and value_is_clear_scalar_integer( node.inputs[0] ): # flip lhs and rhs @@ -120,7 +120,7 @@ def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): def constant(node, _, __, ctx): """Converter function for constant inputs.""" - if not value_is_clear_integer(node.outputs[0]): + if not value_is_clear_scalar_integer(node.outputs[0]): raise TypeError("Don't support non-integer constants") dtype = cast(Integer, node.outputs[0].data_type) if dtype.is_signed: diff --git a/hdk/common/mlir/mlir_converter.py b/hdk/common/mlir/mlir_converter.py index b48d1ca78..1e0124251 100644 --- a/hdk/common/mlir/mlir_converter.py +++ b/hdk/common/mlir/mlir_converter.py @@ -12,8 +12,8 @@ from zamalang.dialects import hlfhe from .. import values from ..data_types import Integer from ..data_types.dtypes_helpers import ( - value_is_clear_integer, - value_is_encrypted_unsigned_integer, + value_is_clear_scalar_integer, + value_is_encrypted_scalar_unsigned_integer, ) from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -50,11 +50,11 @@ class MLIRConverter: Returns: corresponding MLIR type """ - if value_is_encrypted_unsigned_integer(value): + if value_is_encrypted_scalar_unsigned_integer(value): return hlfhe.EncryptedIntegerType.get( self.context, cast(Integer, value.data_type).bit_width ) - if value_is_clear_integer(value): + if value_is_clear_scalar_integer(value): dtype = cast(Integer, value.data_type) if dtype.is_signed: return IntegerType.get_signed(dtype.bit_width, context=self.context) diff --git a/hdk/common/mlir/utils.py b/hdk/common/mlir/utils.py index ff0b5c195..77b374708 100644 --- a/hdk/common/mlir/utils.py +++ b/hdk/common/mlir/utils.py @@ -3,9 +3,9 @@ from typing import cast from ..data_types import Integer from ..data_types.dtypes_helpers import ( - value_is_clear_integer, - value_is_encrypted_integer, - value_is_integer, + value_is_clear_scalar_integer, + value_is_encrypted_scalar_integer, + value_is_scalar_integer, ) from ..operator_graph import OPGraph @@ -21,7 +21,7 @@ def is_graph_values_compatible_with_mlir(op_graph: OPGraph) -> bool: """ return all( all( - value_is_integer(out) and not cast(Integer, out.data_type).is_signed + value_is_scalar_integer(out) and not cast(Integer, out.data_type).is_signed for out in out_node.outputs ) for out_node in op_graph.output_nodes.values() @@ -37,9 +37,9 @@ def _set_all_bit_width(op_graph: OPGraph, p: int): """ for node in op_graph.graph.nodes: for value in node.outputs + node.inputs: - if value_is_clear_integer(value): + if value_is_clear_scalar_integer(value): value.data_type.bit_width = p + 1 - elif value_is_encrypted_integer(value): + elif value_is_encrypted_scalar_integer(value): value.data_type.bit_width = p @@ -52,8 +52,8 @@ def update_bit_width_for_mlir(op_graph: OPGraph): max_bit_width = 0 for node in op_graph.graph.nodes: for value_out in node.outputs: - if value_is_clear_integer(value_out): + if value_is_clear_scalar_integer(value_out): max_bit_width = max(max_bit_width, value_out.data_type.bit_width - 1) - elif value_is_encrypted_integer(value_out): + elif value_is_encrypted_scalar_integer(value_out): max_bit_width = max(max_bit_width, value_out.data_type.bit_width) _set_all_bit_width(op_graph, max_bit_width) diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 05a28bddb..51698bde7 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -6,8 +6,8 @@ from hdk.common.data_types.base import BaseDataType from hdk.common.data_types.dtypes_helpers import ( find_type_to_hold_both_lossy, mix_values_determine_holding_dtype, - value_is_encrypted_integer, - value_is_encrypted_unsigned_integer, + value_is_encrypted_scalar_integer, + value_is_encrypted_scalar_unsigned_integer, ) from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer @@ -37,7 +37,7 @@ from hdk.common.values import ( ) def test_value_is_encrypted_integer(value: BaseValue, expected_result: bool): """Test value_is_encrypted_integer helper""" - assert value_is_encrypted_integer(value) == expected_result + assert value_is_encrypted_scalar_integer(value) == expected_result @pytest.mark.parametrize( @@ -62,7 +62,7 @@ def test_value_is_encrypted_integer(value: BaseValue, expected_result: bool): ) def test_value_is_encrypted_unsigned_integer(value: BaseValue, expected_result: bool): """Test value_is_encrypted_unsigned_integer helper""" - assert value_is_encrypted_unsigned_integer(value) == expected_result + assert value_is_encrypted_scalar_unsigned_integer(value) == expected_result class UnsupportedDataType(BaseDataType): From 66d0c8dd6237b1ab81ba2e555892c9e27312d337 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 23 Aug 2021 14:42:50 +0200 Subject: [PATCH 0133/1104] feat(tracing): update hnumpy to manage tensor types - binary ops and constants support --- hdk/hnumpy/compile.py | 34 ++++++++++++++++++++++++++++++++- hdk/hnumpy/np_dtypes_helpers.py | 16 ++++++++++------ tests/hnumpy/test_tracing.py | 28 ++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index a0326b6a9..2dbe045d4 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple +import numpy from zamalang import CompilerEngine from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset @@ -20,6 +21,32 @@ from ..hnumpy.tracing import trace_numpy_function from .np_dtypes_helpers import get_base_data_type_for_numpy_or_python_constant_data +def numpy_max_func(lhs: Any, rhs: Any) -> Any: + """Compute the maximum value between two values which can be numpy classes (e.g. ndarray). + + Args: + lhs (Any): lhs value to compute max from. + rhs (Any): rhs value to compute max from. + + Returns: + Any: maximum scalar value between lhs and rhs. + """ + return numpy.maximum(lhs, rhs).max() + + +def numpy_min_func(lhs: Any, rhs: Any) -> Any: + """Compute the minimum value between two values which can be numpy classes (e.g. ndarray). + + Args: + lhs (Any): lhs value to compute min from. + rhs (Any): rhs value to compute min from. + + Returns: + Any: minimum scalar value between lhs and rhs. + """ + return numpy.minimum(lhs, rhs).min() + + def compile_numpy_function_into_op_graph( function_to_trace: Callable, function_parameters: Dict[str, BaseValue], @@ -72,7 +99,12 @@ def compile_numpy_function_into_op_graph( ) # Find bounds with the dataset - node_bounds = eval_op_graph_bounds_on_dataset(op_graph, dataset) + node_bounds = eval_op_graph_bounds_on_dataset( + op_graph, + dataset, + min_func=numpy_min_func, + max_func=numpy_max_func, + ) # Update the graph accordingly: after that, we have the compilable graph op_graph.update_values_with_bounds( diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 0cf1792ae..820405005 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -15,7 +15,7 @@ from ..common.data_types.dtypes_helpers import ( ) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer -from ..common.values import BaseValue, ScalarValue +from ..common.values import BaseValue, ScalarValue, TensorValue NUMPY_TO_HDK_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.int32): Integer(32, is_signed=True), @@ -110,11 +110,13 @@ def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> """ base_dtype: BaseDataType assert isinstance( - constant_data, (int, float, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) + constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) ), f"Unsupported constant data of type {type(constant_data)}" - if isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): - base_dtype = convert_numpy_dtype_to_base_data_type(constant_data) + if isinstance(constant_data, (numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)): + # numpy + base_dtype = convert_numpy_dtype_to_base_data_type(constant_data.dtype) else: + # python base_dtype = get_base_data_type_for_python_constant_data(constant_data) return base_dtype @@ -139,11 +141,13 @@ def get_base_value_for_numpy_or_python_constant_data( """ constant_data_value: Callable[..., BaseValue] assert isinstance( - constant_data, (int, float, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) + constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) ), f"Unsupported constant data of type {type(constant_data)}" base_dtype = get_base_data_type_for_numpy_or_python_constant_data(constant_data) - if isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): + if isinstance(constant_data, numpy.ndarray): + constant_data_value = partial(TensorValue, data_type=base_dtype, shape=constant_data.shape) + elif isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): constant_data_value = partial(ScalarValue, data_type=base_dtype) else: constant_data_value = get_base_value_for_python_constant_data(constant_data) diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 3405d13bc..529b38fd5 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -7,7 +7,7 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.representation import intermediate as ir -from hdk.common.values import ClearValue, EncryptedValue +from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, EncryptedValue from hdk.hnumpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] @@ -114,6 +114,32 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) +@pytest.mark.parametrize( + "tensor_constructor", + [ + EncryptedTensor, + ClearTensor, + ], +) +def test_hnumpy_tracing_tensor_constant(tensor_constructor): + "Test hnumpy tracing tensor constant" + + def simple_add_tensor(x): + return x + numpy.array([[1, 2], [3, 4]], dtype=numpy.int32) + + op_graph = tracing.trace_numpy_function( + simple_add_tensor, {"x": tensor_constructor(Integer(32, True), shape=(2, 2))} + ) + + constant_inputs = [node for node in op_graph.graph.nodes() if isinstance(node, ir.Constant)] + assert len(constant_inputs) == 1 + + constant_input_data = constant_inputs[0].constant_data + + assert (constant_input_data == numpy.array([[1, 2], [3, 4]], dtype=numpy.int32)).all() + assert op_graph.get_ordered_outputs()[0].outputs[0].shape == constant_input_data.shape + + @pytest.mark.parametrize( "function_to_trace,op_graph_expected_output_type,input_and_expected_output_tuples", [ From 2585eb7ed81e9ecb265519d04ae3edf8be13d2e2 Mon Sep 17 00:00:00 2001 From: youben11 Date: Tue, 24 Aug 2021 12:44:45 +0100 Subject: [PATCH 0134/1104] tests: result correctness of the compiled function --- tests/hnumpy/test_compile.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 67f93dc3a..939d3f42c 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -98,6 +98,38 @@ def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_ compiler_engine.run(*args) +@pytest.mark.parametrize( + "function,input_ranges,list_of_arg_names", + [ + pytest.param(lambda x: x + 64, ((0, 10),), ["x"]), + pytest.param(lambda x: x * 3, ((0, 40),), ["x"]), + pytest.param(lambda x: 120 - x, ((40, 80),), ["x"]), + pytest.param(lambda x, y: x + y + 64, ((0, 20), (0, 20)), ["x", "y"]), + pytest.param(lambda x, y: 100 - y + x, ((0, 20), (0, 20)), ["x", "y"]), + pytest.param(lambda x, y: 50 - y * 2 + x, ((0, 20), (0, 20)), ["x", "y"]), + ], +) +def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): + """Test correctness of results when running a compiled function""" + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedValue(Integer(64, False)) for arg_name in list_of_arg_names + } + + compiler_engine = compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + args = [random.randint(low, high) for (low, high) in input_ranges] + assert compiler_engine.run(*args) == function(*args) + + def test_compile_function_with_direct_tlu(): """Test compile_numpy_function for a program with direct table lookup""" From b41029d9c006c5ff08975f705c9fd349bc37b98e Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 20 Aug 2021 17:09:19 +0300 Subject: [PATCH 0135/1104] refactor(drawing): start using graphviz for visualization --- .github/workflows/continuous-integration.yaml | 2 + docker/Dockerfile | 3 +- examples/QuantizedLinearRegression.ipynb | 28 +- examples/QuantizedLogisticRegression.ipynb | 66 +++-- hdk/common/compilation/artifacts.py | 3 +- hdk/common/debugging/drawing.py | 247 ++++-------------- hdk/common/representation/intermediate.py | 27 ++ hdk/hnumpy/tracing.py | 4 +- poetry.lock | 44 ++-- pyproject.toml | 2 + tests/hnumpy/test_compile.py | 2 +- tests/hnumpy/test_debugging.py | 6 +- 12 files changed, 186 insertions(+), 248 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index f257e30a0..d2931efe7 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -24,6 +24,8 @@ jobs: steps: - name: Install Git run: apt-get install git -y + - name: Install Graphviz + run: apt-get install graphviz* -y - name: Checkout Code uses: actions/checkout@v2 with: diff --git a/docker/Dockerfile b/docker/Dockerfile index 71a27ddfd..b3db5a053 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,7 @@ FROM ghcr.io/zama-ai/zamalang-compiler -RUN apt-get install --no-install-recommends -y python3.8 python3.8-venv python-is-python3 git && \ +RUN apt-get install --no-install-recommends -y \ + python3.8 python3.8-venv python-is-python3 git graphviz* && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry && \ echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index f5e4cf690..a552f080b 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -641,7 +641,7 @@ "id": "f0b08a0f", "metadata": {}, "source": [ - "### Here is the textual representation of the operation graph" + "### Here are some representations of the operation graph" ] }, { @@ -670,6 +670,28 @@ "print(get_printable_graph(homomorphic_model, show_data_types=True))" ] }, + { + "cell_type": "code", + "execution_count": 21, + "id": "785c50ce", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from hdk.common.debugging import draw_graph\n", + "draw_graph(homomorphic_model).show()" + ] + }, { "cell_type": "markdown", "id": "ade14f17", @@ -682,7 +704,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "dd2d03d7", "metadata": {}, "outputs": [], @@ -705,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "57050b5d", "metadata": {}, "outputs": [ diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index ccf05f67b..834cac1d2 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -173,22 +173,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Epoch: 1 | Loss: 0.5758869647979736\n", - "Epoch: 101 | Loss: 0.13611836731433868\n", - "Epoch: 201 | Loss: 0.08021673560142517\n", - "Epoch: 301 | Loss: 0.05636058747768402\n", - "Epoch: 401 | Loss: 0.043306026607751846\n", - "Epoch: 501 | Loss: 0.03511128947138786\n", - "Epoch: 601 | Loss: 0.029501130804419518\n", - "Epoch: 701 | Loss: 0.025424323976039886\n", - "Epoch: 801 | Loss: 0.02233024500310421\n", - "Epoch: 901 | Loss: 0.01990305446088314\n", - "Epoch: 1001 | Loss: 0.0179488193243742\n", - "Epoch: 1101 | Loss: 0.01634199731051922\n", - "Epoch: 1201 | Loss: 0.014997857622802258\n", - "Epoch: 1301 | Loss: 0.013856985606253147\n", - "Epoch: 1401 | Loss: 0.012876608408987522\n", - "Epoch: 1501 | Loss: 0.012025204487144947\n" + "Epoch: 1 | Loss: 0.9475528597831726\n", + "Epoch: 101 | Loss: 0.13412582874298096\n", + "Epoch: 201 | Loss: 0.07946280390024185\n", + "Epoch: 301 | Loss: 0.05598355457186699\n", + "Epoch: 401 | Loss: 0.04308217763900757\n", + "Epoch: 501 | Loss: 0.034963589161634445\n", + "Epoch: 601 | Loss: 0.02939651347696781\n", + "Epoch: 701 | Loss: 0.025346478447318077\n", + "Epoch: 801 | Loss: 0.022270068526268005\n", + "Epoch: 901 | Loss: 0.019855139777064323\n", + "Epoch: 1001 | Loss: 0.01790979877114296\n", + "Epoch: 1101 | Loss: 0.016309652477502823\n", + "Epoch: 1201 | Loss: 0.014970536343753338\n", + "Epoch: 1301 | Loss: 0.013833633624017239\n", + "Epoch: 1401 | Loss: 0.012856445275247097\n", + "Epoch: 1501 | Loss: 0.012007634155452251\n" ] } ], @@ -289,9 +289,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.53586054]\n", - " [2.37015319]]\n", - "-14.660321235656738\n" + "[[4.53723335]\n", + " [2.37176466]]\n", + "-14.666179656982422\n" ] } ], @@ -748,7 +748,7 @@ "id": "f0b08a0f", "metadata": {}, "source": [ - "### Here is the textual representation of the operation graph" + "### Here are some representations of the operation graph" ] }, { @@ -783,6 +783,28 @@ "print(get_printable_graph(homomorphic_model, show_data_types=True))" ] }, + { + "cell_type": "code", + "execution_count": 21, + "id": "9d1ff32f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbsAAAIbCAYAAABynTBtAAB/gUlEQVR4nO3dd3hU1dbA4d+khw5KDShFAtIEqSJNwYJIEaVKRwQEQcWrKHqxYVe8gtKl+CkdQRFULr1IkaI0L12KFCG0AOnr+2NnMglJIGVmzpT1+uTJmcKcNdu9s07ZxSYiglJKKeW75gZYHYFSSinlaprslFJK+TxNdkoppXxekJU7P3kSdu0yP0eOwIkT5rnTp+HCBUhKgsuXISEB8uSB0FAIC4NChaBkSYiIgFKlIDISqleHKlUgb14rv5F3OQnsSv45ApxIfu40cAFIAi4DCUAeIBQIAwoBJYEIoBQQCVQHqgBa/P5D26+1tP1mj81dHVQSE2HHDli92vysXw/nzjl3HzabaTiNG0OTJtCsGZQp49x9eKtEYAewOvlnPeDk4seGaTiNgSZAM0CL3zdo+7WWtt9cm+vSZBcXB8uXw3ffwaJFcObMjd8fEADFi0OxYlCkCAQGQv78EBQEV69CbCxcuwZRUeYo8tKlm8dw113Qvj089pg5evQnccBy4DtgEXCT4icAKA4UA4oAgUB+zOn/VSAWuAZEYY4is1D83AW0Bx7DHD0q76Ht11rafp3KNcnuwAGYOBGmToWzZ9O/HhhoKnHt2lCtGlStCpUqQYkSpmFk1dWrcPQo7NkDu3ebyykbNsDx4xm/v1YtGDAAunaFfPly9t28wQFgIjAVyKD4CcRU4tpANaAqUAkoQfaua18FjgJ7gN2YyykbgEyKn1rAAKAr4MPF7/W0/VpL269LODfZbdwIb78NS5fC9Z9arhy0awfNm0OjRlCwoLP2mt7Bg7BmjYlj6VKIjk77eoEC0L8/vPiiOQr1FRuBt4GlwPX/U8sB7YDmQCPAhcXPQWBNchxLgeuKnwJAf+BFzFGo8gzafq2l7del5iJO8NtvIg8/LGKaiOOnRAmR4cNFtm1zxl5y5to1kUWLRDp2FAkJSRtf3rwiL74oEhVlXXzO8JuIPCwiXPdTQkSGi4iFxS/XRGSRiHQUkRBJG19eEXlRRLy8+L2etl9raft1izm5Snbnz4sMGiQSGJi2Et57r8icOSJxcU4K00lOnRJ5912RkiXTxlusmMi0aSJJSVZHmD3nRWSQiARK2kp4r4jMEREPK345JSLvikhJSRtvMRGZJiJeVvxeT9uvtc6Ltl83ynmy+/FHkeLF01a6Jk1Eli93ZnyucfWqyGefpW80zZqJHDtmdXRZ86OIFJe0la6JiHhB8ctVEflM0jeaZiLiJcXv9bT9Wkvbr9tlP9nFxYmMHCkSEOCoZKVKiUyf7oLwXOzKFfNdQkMd36VgQXNU66niRGSkiASIo5KVEhEvLH65Iua7hIrjuxQUc1SrXEPbr7W0/Vome8kuKkqkcWNHxbLZRIYOFYmOdlF4brJnj0j9+mm/1zvvWB1VelEi0lgcFcsmIkNFxMuLX/aISH1J+708sPi9nrZfa2n7tVTWk93RoyJVq6a9Tr5kiStjc6+4OJGXX057xDtwoEhCgtWRGUdFpKqkvU7uQ8UvcSLysqQ94h0oIh5S/F5P26+1tP1aLmvJ7uRJkQoVHJWoenXvuTaeXd99JxIe7viu/fpZf+P7pIhUEEclqi4efW08V74TkXBxfNd+4vE3vj2etl9rY9L26xFunuwuXBCpWdNReZo2Nb24fNm6dSJFiji+84gR1sVyQURqiqPyNBXTi8uXrRORIuL4zhYWv9fT9qvt1908tP3ePNm1beuoNA0aeP/1/azauNGM47F/91mzrImjrTgqTQPx/uv7WbVRzDge+3e3qPi9nrZfbb9W8MD2O+eGS/yMG2fmxAOoXBkWL/afWcnr14d58xzTH/Xvb2Z2d6dxmDnxACoDi/HtWclTqw/MwzH9UX/MzO4q67T9avu1iie230yT3ZEjMGyY2Q4Lgzlz4JZb3BSVh3j4YXj9dbN98SL07eu+fR8BkoufMGAO4GfFz8NAcvFzEXBj8Xs9bb/afq3mae0302Q3YoSZoRzg/fc9d8bxHTt20KpVKwoVKkT+/Plp0aIF69evd9rnjxgB995rtlesMEfH7jACM0M5wPu4f8bx+Ph4Ro8eTe3atcmfPz/FihWjZcuW/PDDD4i4ZVUowJRDcvGzAnN0rG7OW9ovwJIlS4iMjCQoO7NIZ5G2X22/KTK6uLltmxmrAiLVqnlO993rbdy4UcLDw6VTp07y999/yz///CP9+vWToKAg+fnnn522n23bHF2aq1Z1fe+ubWLGqiAi1cT93Xejo6OlUaNGUqNGDVm9erVcvXpV/vrrL3niiScEkJ07d7o1nm3i6NJcVTymd5fH8pb2e+DAAWndurXUqFFDChQoIIGBgS7Zj7Zfbb+SWQeVfv0cN3YXL3Z3TFmTmJgoVatWlZIlS8rVq1dTnk9ISJBKlSpJmTJlJCYmxmn7e/JJR5msXOm0j81QP3Hc2LWi+AcOHCgFChSQU6dOpXk+OjpaQkND3d5YRESeFEeZrHT73r2LN7RfEZEuXbrIe++9J/Hx8RIREeGyZCei7VfE79tv+mR39apIoUKmUpQvb/0YlcysXLlSAHn22WfTvfbGG28IIPPmzXPa/jZtcjSW7t2d9rHpXBWRQmIqRXlx/1HQqVOnJDAwUAYOHOjmPd/YJnE0FhcWv9fzlvYrImkOUl2d7LT9WssD2m/63pjLlsGFC2a7b1+w2dx4TTUbVqxYAUCdOnXSvWZ/bvny5U7bX716jvseCxdCYqLTPjqNZcCF5O2+gLuL//vvvycxMZFGjRq5ec83Vg/HfY+FgIuK3+t5S/sFCA8Pd9u+tP1ayxPab7pkt26dY/uRR5yzk0aNGmGz2VJ+unXrBkCLFi3SPH/B3kqz4M8//wSgdOnS6V6LiIgAYN++fbkPPhV7eVy+DH/84dSPTpGq+HFS8WfLtm3bAChcuDDDhg2jTJkyhISEcPvttzNkyBCioqIsiMqwl8dlwEXF7/W8pf1aQduvf7ffdMlu0ybzO18+5/XgWrduHTt27CBv3rzcddddTJgwAYAff/yR+vXrM3PmTESEQoUKZfkz7Q0rbwYDh/LlM4vGnz9/Ptexp9awoWN740anfnSK5OInH+7vwQVw8uRJAPr06cPp06dZvXo1Z86c4e233+arr77innvu4eLFixZEBqmKHxcVv9fzlvZrBW2//t1+0yW748fN78hICAx03o7uuusupk6dyu+//06PHj0QEfr370/z5s3p3Lmz83YEKV1rbU6+hlOlimPbXk7OZv/YSMCJxZ9lMTExgLnENG3aNMqXL0+hQoXo0aMHr7zyCvv27eOTTz6xIDJIVfy4qPi9ni+0X1fR9uvf7Tddsjt71vwuWtT5O+vQoQMjRoxgwYIFNGrUiHPnzvH222/n6LPsR5FXrlxJ95r9OWcfaaYelGsvJ2ezf6wLij9L7GfKLVq0SDfuqXXr1gD8/PPPbo8L0g7KdVHxez1vab9W0Pbr3+03XbKzD0R11b3jt99+m/r167NhwwY6dOhAQMANZyzLVOXKlQE4nsEh2okTJwCIjIzMeaAZSH3FNIMc6xT2gajuu3WfVtmyZQG4JYPpNooVKwbAP//8486QUqS+YO2i4vd63tJ+raDt17/bb7qaWriw+e3k210pVq1axcWLF6levTrPPPMMv//+e44+57777gNg69at6V6zP9e8efOcB5qBc+cc266aeim5+HFR8d+UvReX/dp/amfOnAGgePHibo3JLlXx+93US1nlLe3XCtp+/bv9pkt29kpw+rTzd3b48GH69u3L/Pnz+f777wkPD6dt27Y5OtJo2rQpVapUYd68eSnXqQESExOZNWsWZcqUoVWrVs4Mn+S6AkCRIk796BT2SuCC4s+SRx55hIiICH766ac05Qrwww8/ANCuXTsLIoNUxY+Lit/reUv7tYK2Xz9vv9ePvGvXzgy8DAgwa2E5y+XLl6VGjRqyaNGilOdWrVolwcHB0qRJE4mLi8v2Z/76668SFhYmnTt3lpMnT8rZs2elf//+EhQUJD/99JPzgk82aZJjYOrs2U7/eBERaSdm4GWAmLWwrLB06VIJCgqStm3byr59++T8+fMyY8YMyZs3r9SvXz/NYGB3miSOgakuKn6v503tNzVXDyoX0fbr5+03/QwqH33kqBDOml5y0KBBAqT87Ny5U/755580zwHy9ttvZ/uzt23bJi1btpQCBQpIvnz55P7775d169Y5J/Dr9O7tKJujR12yC/lIHBXCebN7Zt+GDRvkoYcekoIFC0pISIhUrlxZ3njjDcsaiohIb3GUjYuK3+t5U/v94Ycf0n2G/WfSpEnOCT4Vbb9+3X7n2ETSToG9ebNZCwrMDAyTJ7v63NI7xMRARARERUG5cnDokGv2sxmzFhSYGRi0+I0YIAKIAsoBLip+r6ftN2Pafq3lAe13brp7dnXrmjE6ALNnQ3S0u2PyTAsXmoYC0L276/ZTFzNGB2A2oMVvLMQ0FAAXFr/X0/abMW2/1lqI9e03XbKz2aBXL7MdHQ3/+Y+bI/JASUnw4Ydm22aDnj1dty8b0Ct5OxrQ4ockILn4sQEuLH6vp+03PW2/1vKU9pvhIJn+/R1dmD/8MG0vJldLPddeZj9vvPGG+wICZsyA7dvNdqdOUL68a/fXH0cX5g9J24vJH80AkoufToCLi9/raftNS9uvtTym/WZ2Ny/1je727d13F9HTnDolUry4KYeQEJEDB9yz39Q3uv24+OWUiBQXUw4hIuKm4vd62n4Nbb/W8qD2m36JH7vBgyF5khIWLIBJk9yVfj1HUhI8+aRjzNLzz0OFCu7Z92AgufhZAPhh8ZMEPIljzNLzgJuK3+tp+9X2azWPa783SoV//CESFuY4KnJWV2Zv8dxzjqPj2rVFYmPdu/8/RCRMHEdFflb88pw4jo5ri4ibi9/rafvV9mslD2u/mZ/ZgVkixH5jNy4OOnSADGbn8klvvQWffWa2CxeGWbMgJMS9MVTHcWM3DugA+Enx8xbwWfJ2YWAW4Obi93rafs22tl/388j2m5WUOGyY4wgpXz4RF0xO4jGSkkRGjnR83/BwkTVrrI1pmDiOkPKJiA8XvySJyEhxfN9wEbG4+L2etl9rY9L26xHSz6CSkaQkkZ49HRUoNFRk+nQXh2aBK1dEunRxfM+QEJFUsyNZJklEeoqjAoWKiA8Wv1wRkS7i+J4hIuIBxe/1tP1aS9uvR8hashNJf8QEIt27i1y+7MLw3GjPHpHq1dMeAS9danVUDtcfMSEi3UXER4pf9ohIdUl7BOxBxe/1tP1aS9uv5bKe7OxGjxYJCnJUqooVRX75xQWhuUlMjMioUeZyh/07lSkjsn271ZFlbLSIBImjUlUUES8ufokRkVFiLnfYv1MZEdluYUy+TNuvtUaLtl+LZD/ZiYisXy9y++1pjxI7dBA5fNi50bnajz+KREam/R5t24qcO2d1ZDe2XkRul7RHiR1E5LB1IeXIjyISKWm/R1sR8fDi93rafq2l7dcSOUt2IiJRUSI9eojYbI6KFhws8tRTIocOOTNG51u6VKRBg7SNpFAhkXHjzOUebxAlIj1ExCaOihYsIk+JiIcXvywVkQaStpEUEpFxYi73KNfT9mstbb9ul/NkZ7dmTdpr5fa1tFq0EJkzRyQhwRlx5t7FiyITJojUrJk2VvtR7alTVkeYM2sk7bVyxKyl1UJE5oiIhxS/XBSRCSJSU9LGaj+q9dLi93rafq2l7ddtcp/sRETi40WmTRO54470FbFsWZF//Utk40b3H3VFR4vMnSvSubNI3rxp47LZRFq3FvntN/fG5ArxIjJNRO6Q9BWxrIj8S0Q2ivuPuqJFZK6IdBaRvNfFZROR1iLiA8Xv9bT9Wkvbr1ukX88uNxISYOZMGDvWrKt1vYgIuP9+aNIEGjeGSpWctWcjNha2bIHVq2HtWlizBq5dS/ue0FB4/HF44QWoXdu5+7daAjATGItZV+t6EcD9QBOgMeDk4icW2AKsBtYCa4Drip9Q4HHgBcDHit/rafu1lrZfl5rr1GSX2vbtMHEizJsHZ89m/J4CBaBKFTPTQ2QklCwJZcpA8eLmtbAwyJvXzHwQHQ3x8XDpkvk5dszMeffXX7BnD+zaBfv3mwZrJAEHgYoAVKsGPXpA795w662u+MaeZTswEZgHZFL8FACqYGZ6iARKAmWA4smvhQF5MTMfRAPxwKXkn2OYOe/+AvYAu4D9mAabkWpAD6A34AfF7/Wsb79pafsF9u1zLFaItt9scl2ys0tMNEdqCxbAzz/DgQOu3JtDYOBHBAR8xPDhe+nW7ZbUdcSvJGKO1BYAPwNuKn6CMQtZtgba41jQUnkXq9pvcLBZiLZ1a2jfHr9vvx/Nm8fPXbog//uf69cowifbr+uT3fX+/ts0ng0bzNHczp1w7lzuPjMgAMqWNUeYNWuaSyxVq16iTp0qPPDAA0ydOtUZofuEvzGNZwPmaG4nkFL8R47Af/9rlnIODc3yZwYAZTFHmDUxl1gaYI4qlW9xTvs9CCwDBgAZt98GDcxZoYKLFy9StWpVHn74Yd6aPDnz9ptDftJ+3Z/sMnLqFBw6BCdPwokT5vLG5cvmGv7Vq+Z3/vwQFAT58plLJBER5rJJRARUrJhxw1iwYAGPP/44v/zyCw888ID7v5iXOAUcAmZMnMjUF17ghQsXuBIURCxwFXMtPz8QBOTDXCKJwFw2icBcKPaxhqGyIbvt9+TJ75k1qx0zZ56jUqXCmbZfZTz99NN899137N27l1szuIZrb78ngROYy5OXQdtvWp6R7Fypffv2bN++nV27dpFXW9QN9evXj/3797Nq1SqrQ1E+7MyZMxQvXlwPQrNg7dq1NG3alG+//ZbOnTtbHY43m3vDJX58wRdffMGFCxd45513rA7F423atIn69etbHYbyccWKFeP2229nc0ZdPlWK2NhYBgwYwMMPP6yJzgl8PtmVLFmSUaNG8fHHH7N9+3arw/FYV65cYe/evdStW9fqUJQfqFevnia7m3j33Xc5cuQIX3zxhdWh+ASfT3YAAwYMoEGDBvTv35/ExESrw/FIv/32GwkJCdSrV8/qUJQfqFu3Lhs3brQ6DI/1v//9jw8++IBRo0ZRrlw5q8PxCX6R7AICApg8eTJ//PEHY8aMsTocj7R582ZKlCjBbbfdZnUoyg/Ur1+fM2fOcPToUatD8ThJSUk89dRT1KhRg2effdbqcHyGXyQ7gEqVKvHSSy8xYsQIDh8+bHU4HmfLli16Vqfcpk6dOgQFBemlzAyMHz+ejRs3MmHCBAIDA60Ox2f4TbIDGDFiBGXLlmXQoEFWh+JxNm3apMlOuU2ePHmoUqWKJrvrnDx5khEjRvDiiy9Sq1Ytq8PxKX6V7EJDQxk/fjw//fQTs2bNsjocj2G/nKTJTrmTdlJJb9CgQRQqVIjXXnvN6lB8jl8lO4DGjRvz1FNP8eyzz3I2s0n//MzGjRux2WzUqVPH6lCUH6lbt25KxygFixcv5rvvvmPixIk6JtgF/C7ZAXz00UeEhoYyfPhwq0PxCFu2bCEyMpLChQtbHYryI/Xr108Z8uLvLl26xMCBA+nZs6cOtHcRv0x2BQsWZPTo0Xz11VcsX77c6nAsp/frlBWqVatG3rx52bRpk9WhWG748OHExMTw0UcfWR2Kz/LLZAfQoUMH2rRpw8CBA7l2/aJZfkRE+O233zTZKbcLDAzk7rvvZsuWLVaHYqlNmzYxYcIEPvvsM4oWLWp1OD7Lb5MdwJdffsmZM2cYNWqU1aFYZt++fZw/f16TnbKEv3dSiYuLo2/fvjzwwAM8+eSTVofj0/w62ZUqVYq3336bDz74gB07dlgdjiU2b95MSEgId911l9WhKD9Ur149du7cyZUrV6wOxRLvv/8+hw8f5ssvv7Q6FJ/n18kOTFffevXq+e1UYlu2bKFmzZqEZmP9OqWcpV69eiQmJvrlvLX79u3jvffe46233qK8GxZk9Xd+n+wCAgKYMGECO3bs8MujK13pQFmpbNmyFC9e3O86qYgIAwcOpFKlSgwZMsTqcPyC3yc7ML3C/vWvf/HKK69w5MgRq8Nxm7i4OH7//Xdd6UBZqm7dun7XSWXSpEmsXr2aKVOmEBwcbHU4fkGTXbLXX3+d2267za+mEtuxYwexsbHaOUVZyt86qZw6dYrhw4fz/PPPU7t2bavD8Rua7JKFhoYybtw4li5dyty5c60Oxy02b95MwYIFqVixotWhKD9Wr149Dh8+zOnTp60OxS2effZZChQowMiRI60Oxa9oskuladOm9OnThyFDhnD+/Hmrw3E5+0oHAQFaDZR16tati81m84tLmUuWLGHevHl88cUX5MuXz+pw/Ir+lbvOJ598QkBAAC+//LLVobicdk5RnqBIkSLccccdPp/sLl++zIABA+jWrRutWrWyOhy/o8nuOgULFuTTTz9l8uTJrFixwupwXObixYvs379fO6coj+AP9+1effVVrl69yieffGJ1KH5Jk10GOnXqROvWrRk4cCAxMTFWh+MSmzdvJikpSZOd8gj2ZCciVofiEps3b2bcuHF8+umnFCtWzOpw/JImu0yMGTOGkydP8u677zr1c3fs2EGrVq0oVKgQ+fPnp0WLFqxfv96p+8iKzZs3c9ttt1GyZMksvX/JkiVERkYSFBTk4siUP6pXrx5RUVEcPHgwW//OU9rTjSQkJNC/f3+aNGlC9+7dM32ftjHX0mSXidtuuy1lKrHdu3c75TM3bdpEw4YNyZ8/P3v37uXw4cOUL1+eZs2a8csvvzhlH1ll75xyMwcPHqRNmza88sorftNbTrlfrVq1CA0Nzdbgck9qTzfywQcf8L///Y9JkyZhs9nSva5tzE1EZSoxMVHuueceqV+/viQmJub6s6pWrSolS5aUq1evpjyfkJAglSpVkjJlykhMTExuQ86yUqVKyYcffnjT93Xp0kXee+89iY+Pl4iICAkMDHRDdMof1alTR4YOHZql93pae8rMvn37JCwsTD744INM36NtzC3m6JndDdinEtu2bRvjx4/P1WetWbOG3bt388QTTxAeHp7yfGBgIF26dOHYsWMsXrw4tyFnydGjR/n777+zdGY3ZcoUhg8frpdWlMtlp5OKJ7WnzEjylGCRkZE8//zzmb5P25h7aLK7ierVqzNs2DBeeeUVjh8/nuPPsffsrFOnTrrX7M+5ayHZzZs3ExAQwN13333T96b+Q6KUK9WrV4/t27cTFxd30/d6UnvKzFdffcXKlSuZMGHCDacE0zbmHprssuCNN96gZMmSDBgwIMef8eeffwJQunTpdK9FREQAZhZ0d9iyZQtVq1Ylf/78btmfUllRr149YmJi+OOPP276Xk9qTxk5e/Ysw4cPZ+jQoTRo0MCyOJSDJrssCA0NZfz48SxZsoQFCxbk6DMuXLgAQN68edO9Zp9JwV2ztmzevFnnw1Qep3LlyhQqVChLlzI9qT1lZPDgweTJk4e33nrLshhUWprssqhZs2b07NmTQYMGpTQ0Z5HksUUZ9dRytqSkJLZt26bJTnkcm81G7dq1cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from hdk.common.debugging import draw_graph\n", + "draw_graph(homomorphic_model).show()" + ] + }, { "cell_type": "markdown", "id": "ade14f17", @@ -795,7 +817,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "dd2d03d7", "metadata": {}, "outputs": [], @@ -818,7 +840,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "57050b5d", "metadata": {}, "outputs": [ diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index 63f9f665d..32bb585ae 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -72,9 +72,8 @@ class CompilationArtifacts: draw_graph( self.operation_graph, + show=False, save_to=output_directory.joinpath("graph.png"), - block_until_user_closes_graph=False, - draw_edge_numbers=True, ) if self.bounds is not None: diff --git a/hdk/common/debugging/drawing.py b/hdk/common/debugging/drawing.py index c41c648c2..0f854f27d 100644 --- a/hdk/common/debugging/drawing.py +++ b/hdk/common/debugging/drawing.py @@ -1,10 +1,12 @@ """functions to draw the different graphs we can generate in the package, eg to debug.""" +import tempfile from pathlib import Path -from typing import Dict, List, Optional +from typing import Optional import matplotlib.pyplot as plt import networkx as nx +from PIL import Image from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -22,222 +24,71 @@ IR_NODE_COLOR_MAPPING = { } -def human_readable_layout(graph: nx.Graph, x_delta: float = 1.0, y_delta: float = 1.0) -> Dict: - """Returns positions for graphs, to make them easy to read. - - Returns a pos to be used later with eg nx.draw_networkx_nodes, so that nodes - are ordered by depth from input along the x axis and have a uniform - distribution along the y axis - - Args: - graph (nx.Graph): The graph that we want to draw - x_delta (float): Parameter used to set the increment in x - y_delta (float): Parameter used to set the increment in y - - Returns: - pos (Dict): the argument to use with eg nx.draw_networkx_nodes - - """ - nodes_depth = {node: 0 for node in graph.nodes()} - input_nodes = [node for node in graph.nodes() if len(list(graph.predecessors(node))) == 0] - - # Init a layout so that unreachable nodes have a pos, avoids potential crashes wiht networkx - # use a cheap layout - pos = nx.random_layout(graph) - - curr_x = 0.0 - curr_y = -(len(input_nodes) - 1) / 2 * y_delta - - for in_node in input_nodes: - pos[in_node] = (curr_x, curr_y) - curr_y += y_delta - - curr_x += x_delta - - curr_nodes = input_nodes - - current_depth = 0 - while len(curr_nodes) > 0: - current_depth += 1 - next_nodes_set = set() - for node in curr_nodes: - next_nodes_set.update(graph.successors(node)) - - curr_nodes = list(next_nodes_set) - for node in curr_nodes: - nodes_depth[node] = current_depth - - nodes_by_depth: Dict[int, List[int]] = {} - for node, depth in nodes_depth.items(): - nodes_for_depth = nodes_by_depth.get(depth, []) - nodes_for_depth.append(node) - nodes_by_depth[depth] = nodes_for_depth - - depths = sorted(nodes_by_depth.keys()) - - for depth in depths: - nodes_at_depth = nodes_by_depth[depth] - - curr_y = -(len(nodes_at_depth) - 1) / 2 * y_delta - for node in nodes_at_depth: - pos[node] = (curr_x, curr_y) - curr_y += y_delta - - curr_x += x_delta - - return pos - - -def adjust_limits(): - """Increases the limits of x and y axis of the current pyplot figure by 20%. - - Returns: - None - """ - - x_lim = plt.xlim() - x_distance = x_lim[1] - x_lim[0] - plt.xlim([x_lim[0] - x_distance / 10, x_lim[1] + x_distance / 10]) - - y_lim = plt.ylim() - y_distance = y_lim[1] - y_lim[0] - plt.ylim([y_lim[0] - y_distance / 10, y_lim[1] + y_distance / 10]) - - def draw_graph( opgraph: OPGraph, - block_until_user_closes_graph: bool = True, - draw_edge_numbers: bool = True, + show: bool = False, + vertical: bool = True, save_to: Optional[Path] = None, -) -> None: - """Draw a graph. +) -> Image.Image: + """Draws operation graphs and optionally saves/shows the drawing. Args: - opgraph (OPGraph): The graph that we want to draw - block_until_user_closes_graph (bool): if True, will wait the user to - close the figure before continuing; False is useful for the CI tests - draw_edge_numbers (bool): if True, add the edge number on the arrow - linking nodes, eg to differentiate the x and y in a Sub coding - (x - y). This option is not that useful for commutative ops, and - may make the picture a bit too dense, so could be deactivated - save_to (Optional[Path]): if specified, the drawn graph will be saved - to this path + opgraph (OPGraph): the graph to be drawn and optionally saved/shown + show (bool): if set to True, the drawing will be shown using matplotlib + vertical (bool): if set to True, the orientation will be vertical + save_to (Optional[Path]): if specified, the drawn graph will be saved to this path Returns: - None + Pillow Image of the drawn graph. + This is useful because you can use the drawing however you like. + (check https://pillow.readthedocs.io/en/stable/reference/Image.html for further information) """ - assert isinstance(opgraph, OPGraph) - set_of_nodes_which_are_outputs = set(opgraph.output_nodes.values()) - graph = opgraph.graph - # Positions of the node - pos = human_readable_layout(graph) - - # Colors and labels - def get_color(node): + def get_color(node, output_nodes): value_to_return = IR_NODE_COLOR_MAPPING[type(node)] - if node in set_of_nodes_which_are_outputs: + if node in output_nodes: value_to_return = IR_NODE_COLOR_MAPPING["output"] elif isinstance(node, ir.ArbitraryFunction): value_to_return = IR_NODE_COLOR_MAPPING.get(node.op_name, value_to_return) return value_to_return - color_map = [get_color(node) for node in graph.nodes()] + graph = opgraph.graph + output_nodes = set(opgraph.output_nodes.values()) - # For most types, we just pick the operation as the label, but for Input, - # we take the name of the variable, ie the argument name of the function - # to compile - def get_proper_name(node): - if isinstance(node, ir.Input): - return node.input_name - if isinstance(node, ir.Constant): - return str(node.constant_data) - if isinstance(node, ir.ArbitraryFunction): - return node.op_name - return node.__class__.__name__ + attributes = { + node: { + "label": node.label(), + "color": get_color(node, output_nodes), + "penwidth": 2, # double thickness for circles + "peripheries": 2 if node in output_nodes else 1, # double circle for output nodes + } + for node in graph.nodes + } + nx.set_node_attributes(graph, attributes) - label_dict = {node: get_proper_name(node) for node in graph.nodes()} + for edge in graph.edges(keys=True): + idx = graph.edges[edge]["input_idx"] + graph.edges[edge]["label"] = f" {idx} " # spaces are there intentionally for a better look - # Draw nodes - nx.draw_networkx_nodes( - graph, - pos, - node_color=color_map, - node_size=1000, - alpha=1, - ) + agraph = nx.nx_agraph.to_agraph(graph) + agraph.graph_attr["rankdir"] = "TB" if vertical else "LR" + agraph.layout("dot") - # Draw labels - nx.draw_networkx_labels(graph, pos, labels=label_dict) + if save_to is None: + with tempfile.NamedTemporaryFile(suffix=".png") as tmp: + agraph.draw(tmp.name) + img = Image.open(tmp.name) + else: + agraph.draw(save_to) + img = Image.open(save_to) - current_axes = plt.gca() + if show: # pragma: no cover + # We can't have coverage in this branch as `plt.show()` blocks and waits for user action. + plt.close("all") + plt.figure() + plt.imshow(img) + plt.axis("off") + plt.show() - # And draw edges in a way which works when we have two "equivalent edges", - # ie from the same node A to the same node B, like to represent y = x + x - already_done = set() - - for e in graph.edges: - - # If we already drew the different edges from e[0] to e[1], continue - if (e[0], e[1]) in already_done: - continue - - already_done.add((e[0], e[1])) - - edges = graph.get_edge_data(e[0], e[1]) - - # Draw the different edges from e[0] to e[1], continue - for which, edge in enumerate(edges.values()): - edge_index = edge["input_idx"] - - # Draw the edge - current_axes.annotate( - "", - xy=pos[e[0]], - xycoords="data", - xytext=pos[e[1]], - textcoords="data", - arrowprops=dict( - arrowstyle="<-", - color="0.5", - shrinkA=5, - shrinkB=5, - patchA=None, - patchB=None, - connectionstyle="arc3,rad=rrr".replace("rrr", str(0.3 * which)), - ), - ) - - if draw_edge_numbers: - # Print the number of the node on the edge. This is a bit artisanal, - # since it seems not possible to add the text directly on the - # previously drawn arrow. So, more or less, we try to put a text at - # a position which is close to pos[e[1]] and which varies a bit with - # 'which' - a, b = pos[e[0]] - c, d = pos[e[1]] - const_0 = 1 - const_1 = 2 - - current_axes.annotate( - str(edge_index), - xycoords="data", - xy=( - (const_0 * a + const_1 * c) / (const_0 + const_1), - (const_0 * b + const_1 * d + 0.1 * which) / (const_0 + const_1), - ), - textcoords="data", - ) - - plt.axis("off") - - adjust_limits() - - # save the figure if requested - if save_to is not None: - plt.savefig(save_to) - - # block_until_user_closes_graph is used as True for real users and False - # for CI - plt.show(block=block_until_user_closes_graph) + return img diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 79a6b9690..1eba9aecf 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -106,6 +106,15 @@ class IntermediateNode(ABC): """ return cls.n_in() > 1 + @abstractmethod + def label(self) -> str: + """Function to get the label of the node. + + Returns: + str: the label of the node + + """ + class Add(IntermediateNode): """Addition between two values.""" @@ -118,6 +127,9 @@ class Add(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] + inputs[1] + def label(self) -> str: + return "+" + class Sub(IntermediateNode): """Subtraction between two values.""" @@ -130,6 +142,9 @@ class Sub(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] - inputs[1] + def label(self) -> str: + return "-" + class Mul(IntermediateNode): """Multiplication between two values.""" @@ -142,6 +157,9 @@ class Mul(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] * inputs[1] + def label(self) -> str: + return "*" + class Input(IntermediateNode): """Node representing an input of the program.""" @@ -173,6 +191,9 @@ class Input(IntermediateNode): and super().is_equivalent_to(other) ) + def label(self) -> str: + return self.input_name + class Constant(IntermediateNode): """Node representing a constant of the program.""" @@ -213,6 +234,9 @@ class Constant(IntermediateNode): """ return self._constant_data + def label(self) -> str: + return str(self.constant_data) + class ArbitraryFunction(IntermediateNode): """Node representing a univariate arbitrary function, e.g. sin(x).""" @@ -257,3 +281,6 @@ class ArbitraryFunction(IntermediateNode): and self.op_name == other.op_name and super().is_equivalent_to(other) ) + + def label(self) -> str: + return self.op_name diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 6b4e40e18..eff5aea12 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -129,7 +129,7 @@ class NPTracer(BaseTracer): arbitrary_func=numpy.rint, output_dtype=common_output_dtypes[0], op_kwargs=deepcopy(kwargs), - op_name="numpy.rint", + op_name="np.rint", ) output_tracer = self.__class__( input_tracers, traced_computation=traced_computation, output_index=0 @@ -151,7 +151,7 @@ class NPTracer(BaseTracer): arbitrary_func=numpy.sin, output_dtype=common_output_dtypes[0], op_kwargs=deepcopy(kwargs), - op_name="numpy.sin", + op_name="np.sin", ) output_tracer = self.__class__( input_tracers, traced_computation=traced_computation, output_index=0 diff --git a/poetry.lock b/poetry.lock index 1512e87b8..681ef239b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -229,7 +229,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.3.1" +version = "6.3.3" description = "Run coverage and linting reports on diffs" category = "dev" optional = false @@ -511,13 +511,14 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "6.2.0" +version = "7.0.1" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false python-versions = ">=3.6.1" [package.dependencies] +entrypoints = "*" jupyter-core = ">=4.6.0" nest-asyncio = ">=1.5" python-dateutil = ">=2.1" @@ -526,8 +527,8 @@ tornado = ">=4.1" traitlets = "*" [package.extras] -doc = ["sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] -test = ["async-generator", "ipykernel", "ipython", "mock", "pytest-asyncio", "pytest-timeout", "pytest", "mypy", "pre-commit", "jedi (<0.18)"] +doc = ["myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"] +test = ["codecov", "coverage", "ipykernel", "ipython", "mock", "mypy", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "pytest-timeout", "jedi (<0.18)"] [[package]] name = "jupyter-console" @@ -974,11 +975,11 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.19" +version = "3.0.20" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" @@ -1068,6 +1069,14 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "pygraphviz" +version = "1.7" +description = "Python interface to Graphviz" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "pylint" version = "2.9.6" @@ -1413,7 +1422,7 @@ test = ["pytest"] [[package]] name = "terminado" -version = "0.11.0" +version = "0.11.1" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." category = "dev" optional = false @@ -1555,7 +1564,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "65489a7f8c03f8825d0948ffec2ef5809e53d82e5b9eb3c77d5fa512c175d0fd" +content-hash = "382f9225cb89e407123521c4a9ec5aedc021701f7af8ef79e2959ca5996a6be1" [metadata.files] alabaster = [ @@ -1756,8 +1765,8 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.3.1-py3-none-any.whl", hash = "sha256:2578fb51c4a5ce162d9ba7f5dcc28132b55539c889e9b648f9df77d4fdcf8fb4"}, - {file = "diff_cover-6.3.1.tar.gz", hash = "sha256:21baf9d6f40ef352df4adf19b5bb4d47249c540a648fecda4647a41ff558d47c"}, + {file = "diff_cover-6.3.3-py3-none-any.whl", hash = "sha256:4aaffc7051dd6b0e4e39170d2a69f412a21bbbf8497c85654a8d0c1fd44be534"}, + {file = "diff_cover-6.3.3.tar.gz", hash = "sha256:487b9babf6d1a7d73b9f72c2ee4cbed2840bf2f0e203e184b9ef632532115665"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1837,8 +1846,8 @@ jupyter = [ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, ] jupyter-client = [ - {file = "jupyter_client-6.2.0-py3-none-any.whl", hash = "sha256:9715152067e3f7ea3b56f341c9a0f9715c8c7cc316ee0eb13c3c84f5ca0065f5"}, - {file = "jupyter_client-6.2.0.tar.gz", hash = "sha256:e2ab61d79fbf8b56734a4c2499f19830fbd7f6fefb3e87868ef0545cb3c17eb9"}, + {file = "jupyter_client-7.0.1-py3-none-any.whl", hash = "sha256:07b9566979546004c089afe7c9bf9e96224ec5f8421fe0ae460759fa593c6b1d"}, + {file = "jupyter_client-7.0.1.tar.gz", hash = "sha256:48822a93d9d75daa5fde235c35cf7a92fc979384735962501d4eb60b197fb43a"}, ] jupyter-console = [ {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, @@ -2182,8 +2191,8 @@ prometheus-client = [ {file = "prometheus_client-0.11.0.tar.gz", hash = "sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, - {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, + {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, + {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -2240,6 +2249,9 @@ pygments = [ {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] +pygraphviz = [ + {file = "pygraphviz-1.7.zip", hash = "sha256:a7bec6609f37cf1e64898c59f075afd659106cf9356c5f387cecaa2e0cdb2304"}, +] pylint = [ {file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"}, {file = "pylint-2.9.6.tar.gz", hash = "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"}, @@ -2472,8 +2484,8 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] terminado = [ - {file = "terminado-0.11.0-py3-none-any.whl", hash = "sha256:221eef83e6a504894842f7dccfa971ca2e98ec22a8a9118577e5257527674b42"}, - {file = "terminado-0.11.0.tar.gz", hash = "sha256:1e01183885f64c1bba3cf89a5a995ad4acfed4e5f00aebcce1bf7f089b0825a1"}, + {file = "terminado-0.11.1-py3-none-any.whl", hash = "sha256:9e0457334863be3e6060c487ad60e0995fa1df54f109c67b24ff49a4f2f34df5"}, + {file = "terminado-0.11.1.tar.gz", hash = "sha256:962b402edbb480718054dc37027bada293972ecadfb587b89f01e2b8660a2132"}, ] testpath = [ {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, diff --git a/pyproject.toml b/pyproject.toml index 5c7eb534c..5d89778af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,8 @@ python = ">=3.7,<3.10" networkx = "^2.6.1" matplotlib = "^3.4.2" numpy = "^1.21.1" +pygraphviz = "^1.7" +Pillow = "^8.3.1" [tool.poetry.dev-dependencies] isort = "^5.9.2" diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 939d3f42c..353bd7ec7 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -61,7 +61,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n # TODO: For the moment, we don't have really checks, but some printfs. Later, # when we have the converter, we can check the MLIR - draw_graph(op_graph, block_until_user_closes_graph=False) + draw_graph(op_graph, show=False) str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) print(f"\n{str_of_the_graph}\n") diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 78aad1c31..65679d228 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -137,7 +137,7 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): x, y = x_y graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) - draw_graph(graph, block_until_user_closes_graph=False) + draw_graph(graph, show=False) str_of_the_graph = get_printable_graph(graph) @@ -167,7 +167,7 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph "Test hnumpy get_printable_graph and draw_graph on graphs with direct table lookup" graph = tracing.trace_numpy_function(lambda_f, params) - draw_graph(graph, block_until_user_closes_graph=False) + draw_graph(graph, show=False) str_of_the_graph = get_printable_graph(graph) @@ -257,7 +257,7 @@ def test_hnumpy_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref """Test hnumpy get_printable_graph with show_data_types on graphs with direct table lookup""" graph = tracing.trace_numpy_function(lambda_f, params) - draw_graph(graph, block_until_user_closes_graph=False) + draw_graph(graph, show=False) str_of_the_graph = get_printable_graph(graph, show_data_types=True) From 4655bea98786f277eb6998285002a2906eed7a3e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 24 Aug 2021 14:43:01 +0200 Subject: [PATCH 0136/1104] fix: register IR nodes to check when nodes are missing debug draw colors --- hdk/common/debugging/drawing.py | 9 +++++++++ hdk/common/representation/intermediate.py | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/hdk/common/debugging/drawing.py b/hdk/common/debugging/drawing.py index 0f854f27d..62a17aba3 100644 --- a/hdk/common/debugging/drawing.py +++ b/hdk/common/debugging/drawing.py @@ -10,6 +10,7 @@ from PIL import Image from ..operator_graph import OPGraph from ..representation import intermediate as ir +from ..representation.intermediate import ALL_IR_NODES IR_NODE_COLOR_MAPPING = { ir.Input: "blue", @@ -23,6 +24,14 @@ IR_NODE_COLOR_MAPPING = { "output": "magenta", } +_missing_nodes_in_mapping = ALL_IR_NODES - IR_NODE_COLOR_MAPPING.keys() +assert len(_missing_nodes_in_mapping) == 0, ( + f"Missing IR node in IR_NODE_COLOR_MAPPING : " + f"{', '.join(sorted(str(node_type) for node_type in _missing_nodes_in_mapping))}" +) + +del _missing_nodes_in_mapping + def draw_graph( opgraph: OPGraph, diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 1eba9aecf..5978ce617 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from copy import deepcopy -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type from ..data_types.base import BaseDataType from ..data_types.dtypes_helpers import ( @@ -13,6 +13,8 @@ from ..values import BaseValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" +ALL_IR_NODES: Set[Type] = set() + class IntermediateNode(ABC): """Abstract Base Class to derive from to represent source program operations.""" @@ -29,6 +31,11 @@ class IntermediateNode(ABC): self.inputs = list(inputs) assert all(isinstance(x, BaseValue) for x in self.inputs) + # Register all IR nodes + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + ALL_IR_NODES.add(cls) + def _init_binary( self, inputs: Iterable[BaseValue], From 6d663ef63d2af0639bf0d3f421da6fd938a7f8d7 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 23 Aug 2021 15:53:52 +0200 Subject: [PATCH 0137/1104] dev(ir): add Dot IntermediateNode --- hdk/common/debugging/drawing.py | 1 + hdk/common/representation/intermediate.py | 65 ++++++++++++++- .../representation/test_intermediate.py | 81 ++++++++++++++++++- 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/hdk/common/debugging/drawing.py b/hdk/common/debugging/drawing.py index 62a17aba3..a4662da8b 100644 --- a/hdk/common/debugging/drawing.py +++ b/hdk/common/debugging/drawing.py @@ -19,6 +19,7 @@ IR_NODE_COLOR_MAPPING = { ir.Sub: "yellow", ir.Mul: "green", ir.ArbitraryFunction: "orange", + ir.Dot: "purple", "ArbitraryFunction": "orange", "TLU": "grey", "output": "magenta", diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 5978ce617..2e563b301 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -9,7 +9,7 @@ from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, mix_scalar_values_determine_holding_dtype, ) -from ..values import BaseValue +from ..values import BaseValue, ClearValue, EncryptedValue, TensorValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" @@ -291,3 +291,66 @@ class ArbitraryFunction(IntermediateNode): def label(self) -> str: return self.op_name + + +def default_dot_evaluation_function(lhs: Any, rhs: Any) -> Any: + """Default python dot implementation for 1D iterable arrays. + + Args: + lhs (Any): lhs vector of the dot. + rhs (Any): rhs vector of the dot. + + Returns: + Any: the result of the dot operation. + """ + return sum(lhs * rhs for lhs, rhs in zip(lhs, rhs)) + + +class Dot(IntermediateNode): + """Node representing a dot product.""" + + _n_in: int = 2 + # Optional, same issue as in ArbitraryFunction for mypy + evaluation_function: Optional[Callable[[Any, Any], Any]] + # Allows to use specialized implementations from e.g. numpy + + def __init__( + self, + inputs: Iterable[BaseValue], + output_dtype: BaseDataType, + delegate_evaluation_function: Optional[ + Callable[[Any, Any], Any] + ] = default_dot_evaluation_function, + ) -> None: + super().__init__(inputs) + assert len(self.inputs) == 2 + + assert all( + isinstance(input_value, TensorValue) and input_value.ndim == 1 + for input_value in self.inputs + ), f"Dot only supports two vectors ({TensorValue.__name__} with ndim == 1)" + + output_scalar_value = ( + EncryptedValue + if (self.inputs[0].is_encrypted or self.inputs[1].is_encrypted) + else ClearValue + ) + + self.outputs = [output_scalar_value(output_dtype)] + self.evaluation_function = delegate_evaluation_function + + def evaluate(self, inputs: Dict[int, Any]) -> Any: + # This is the continuation of the mypy bug workaround + assert self.evaluation_function is not None + return self.evaluation_function(inputs[0], inputs[1]) + + def is_equivalent_to(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and self.evaluation_function == other.evaluation_function + and super().is_equivalent_to(other) + ) + + # TODO: Coverage will come with the ability to trace the operator in a subsequent PR + def label(self) -> str: # pragma: no cover + return "dot" diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 27f6618c4..9283fabd0 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -1,10 +1,12 @@ """Test file for intermediate representation""" +import numpy import pytest +from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.representation import intermediate as ir -from hdk.common.values import ClearValue, EncryptedValue +from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, EncryptedValue @pytest.mark.parametrize( @@ -72,6 +74,46 @@ from hdk.common.values import ClearValue, EncryptedValue 4, id="ArbitraryFunction, x, y -> y[3], where y is constant == (1, 2, 3, 4)", ), + pytest.param( + ir.Dot( + [ + EncryptedTensor(Integer(32, True), shape=(4,)), + ClearTensor(Integer(32, True), shape=(4,)), + ], + Integer(32, True), + ), + [[1, 2, 3, 4], [4, 3, 2, 1]], + 20, + id="Dot, [1, 2, 3, 4], [4, 3, 2, 1]", + ), + pytest.param( + ir.Dot( + [ + EncryptedTensor(Float(32), shape=(4,)), + ClearTensor(Float(32), shape=(4,)), + ], + Float(32), + ), + [[1.0, 2.0, 3.0, 4.0], [4.0, 3.0, 2.0, 1.0]], + 20, + id="Dot, [1.0, 2.0, 3.0, 4.0], [4.0, 3.0, 2.0, 1.0]", + ), + pytest.param( + ir.Dot( + [ + EncryptedTensor(Integer(32, True), shape=(4,)), + ClearTensor(Integer(32, True), shape=(4,)), + ], + Integer(32, True), + delegate_evaluation_function=numpy.dot, + ), + [ + numpy.array([1, 2, 3, 4], dtype=numpy.int32), + numpy.array([4, 3, 2, 1], dtype=numpy.int32), + ], + 20, + id="Dot, np.array([1, 2, 3, 4]), np.array([4, 3, 2, 1])", + ), ], ) def test_evaluate( @@ -191,6 +233,43 @@ def test_evaluate( ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), False, ), + ( + ir.Dot( + [ + EncryptedTensor(Integer(32, True), shape=(4,)), + ClearTensor(Integer(32, True), shape=(4,)), + ], + Integer(32, True), + delegate_evaluation_function=numpy.dot, + ), + ir.Dot( + [ + EncryptedTensor(Integer(32, True), shape=(4,)), + ClearTensor(Integer(32, True), shape=(4,)), + ], + Integer(32, True), + delegate_evaluation_function=numpy.dot, + ), + True, + ), + ( + ir.Dot( + [ + EncryptedTensor(Integer(32, True), shape=(4,)), + ClearTensor(Integer(32, True), shape=(4,)), + ], + Integer(32, True), + delegate_evaluation_function=numpy.dot, + ), + ir.Dot( + [ + EncryptedTensor(Integer(32, True), shape=(4,)), + ClearTensor(Integer(32, True), shape=(4,)), + ], + Integer(32, True), + ), + False, + ), ], ) def test_is_equivalent_to( From ff260b2cd23bdb22b4c6e7fd3b0bbf30e64d862c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 24 Aug 2021 11:52:35 +0200 Subject: [PATCH 0138/1104] feat(nptracer): add dot tracing abilities - remove no cover from Dot.label - small refactor of BaseTracer to make _sanitize a class method - small refactor of get_ufunc_numpy_output_dtype to manage funcs and ufuncs - add function routing to NPTracer - add dot tracing to NPTracer - small refactor to get tracing functions for numpy funcs and ufuncs --- hdk/common/representation/intermediate.py | 3 +- hdk/common/tracing/base_tracer.py | 13 ++-- hdk/hnumpy/np_dtypes_helpers.py | 26 ++++---- hdk/hnumpy/tracing.py | 79 ++++++++++++++++++----- tests/hnumpy/test_debugging.py | 33 +++++++++- tests/hnumpy/test_tracing.py | 62 ++++++++++++++++-- 6 files changed, 176 insertions(+), 40 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 2e563b301..d0cfef740 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -351,6 +351,5 @@ class Dot(IntermediateNode): and super().is_equivalent_to(other) ) - # TODO: Coverage will come with the ability to trace the operator in a subsequent PR - def label(self) -> str: # pragma: no cover + def label(self) -> str: return "dot" diff --git a/hdk/common/tracing/base_tracer.py b/hdk/common/tracing/base_tracer.py index 689d88f9a..851362802 100644 --- a/hdk/common/tracing/base_tracer.py +++ b/hdk/common/tracing/base_tracer.py @@ -53,6 +53,11 @@ class BaseTracer(ABC): def _get_mix_values_func(cls): return cls._mix_values_func + def _sanitize(self, inp) -> "BaseTracer": + if not isinstance(inp, BaseTracer): + return self._make_const_input_tracer(inp) + return inp + def instantiate_output_tracers( self, inputs: Iterable[Union["BaseTracer", Any]], @@ -69,13 +74,9 @@ class BaseTracer(ABC): Returns: Tuple[BaseTracer, ...]: A tuple containing an BaseTracer per output function """ - # For inputs which are actually constant, first convert into a tracer - def sanitize(inp): - if not isinstance(inp, BaseTracer): - return self._make_const_input_tracer(inp) - return inp - sanitized_inputs = [sanitize(inp) for inp in inputs] + # For inputs which are actually constant, first convert into a tracer + sanitized_inputs = [self._sanitize(inp) for inp in inputs] additional_parameters = ( {IR_MIX_VALUES_FUNC_ARG_NAME: self._get_mix_values_func()} diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/hnumpy/np_dtypes_helpers.py index 820405005..d755bcbcc 100644 --- a/hdk/hnumpy/np_dtypes_helpers.py +++ b/hdk/hnumpy/np_dtypes_helpers.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union import numpy from numpy.typing import DTypeLike @@ -154,23 +154,25 @@ def get_base_value_for_numpy_or_python_constant_data( return constant_data_value -def get_ufunc_numpy_output_dtype( - ufunc: numpy.ufunc, +def get_numpy_function_output_dtype( + function: Union[numpy.ufunc, Callable], input_dtypes: List[BaseDataType], ) -> List[numpy.dtype]: - """Function to record the output dtype of a numpy.ufunc given some input types. + """Function to record the output dtype of a numpy function given some input types. Args: - ufunc (numpy.ufunc): The numpy.ufunc whose output types need to be recorded - input_dtypes (List[BaseDataType]): Common dtypes in the same order as they will be used with - the ufunc inputs + function (Union[numpy.ufunc, Callable]): The numpy function whose output types need to + be recorded + input_dtypes (List[BaseDataType]): BaseDataTypes in the same order as they will be used with + the function inputs Returns: - List[numpy.dtype]: The ordered numpy dtypes of the ufunc outputs + List[numpy.dtype]: The ordered numpy dtypes of the function outputs """ - assert ( - len(input_dtypes) == ufunc.nin - ), f"Expected {ufunc.nin} types, got {len(input_dtypes)}: {input_dtypes}" + if isinstance(function, numpy.ufunc): + assert ( + len(input_dtypes) == function.nin + ), f"Expected {function.nin} types, got {len(input_dtypes)}: {input_dtypes}" input_numpy_dtypes = [convert_base_data_type_to_numpy_dtype(dtype) for dtype in input_dtypes] @@ -183,7 +185,7 @@ def get_ufunc_numpy_output_dtype( dtype.type(1000.0 * numpy.random.random_sample()) for dtype in input_numpy_dtypes ) - outputs = ufunc(*dummy_inputs) + outputs = function(*dummy_inputs) if not isinstance(outputs, tuple): outputs = (outputs,) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index eff5aea12..fbd11b881 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,21 +1,21 @@ """hnumpy tracing utilities.""" from copy import deepcopy from functools import partial -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, Optional, Union import numpy from numpy.typing import DTypeLike from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype from ..common.operator_graph import OPGraph -from ..common.representation.intermediate import ArbitraryFunction, Constant +from ..common.representation.intermediate import ArbitraryFunction, Constant, Dot from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from ..common.values import BaseValue from .np_dtypes_helpers import ( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES, convert_numpy_dtype_to_base_data_type, get_base_value_for_numpy_or_python_constant_data, - get_ufunc_numpy_output_dtype, + get_numpy_function_output_dtype, ) SUPPORTED_TYPES_FOR_TRACING = (int, float, numpy.ndarray) + tuple( @@ -39,13 +39,24 @@ class NPTracer(BaseTracer): Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch """ if method == "__call__": - tracing_func = self.get_tracing_func_for_np_ufunc(ufunc) + tracing_func = self.get_tracing_func_for_np_function(ufunc) assert ( len(kwargs) == 0 ), f"hnumpy does not support **kwargs currently for numpy ufuncs, ufunc: {ufunc}" return tracing_func(self, *input_tracers, **kwargs) raise NotImplementedError("Only __call__ method is supported currently") + def __array_function__(self, func, _types, args, kwargs): + """Catch calls to numpy function in routes them to hnp functions if supported. + + Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch + """ + tracing_func = self.get_tracing_func_for_np_function(func) + assert ( + len(kwargs) == 0 + ), f"hnumpy does not support **kwargs currently for numpy functions, func: {func}" + return tracing_func(*args, **kwargs) + def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": r"""Support numpy astype feature. @@ -77,22 +88,27 @@ class NPTracer(BaseTracer): return output_tracer @staticmethod - def get_tracing_func_for_np_ufunc(ufunc: numpy.ufunc) -> Callable: - """Get the tracing function for a numpy ufunc. + def get_tracing_func_for_np_function(func: Union[numpy.ufunc, Callable]) -> Callable: + """Get the tracing function for a numpy function. Args: - ufunc (numpy.ufunc): The numpy ufunc that will be traced + func (Union[numpy.ufunc, Callable]): The numpy function that will be traced Raises: - NotImplementedError: Raised if the passed ufunc is not supported by NPTracer + NotImplementedError: Raised if the passed function is not supported by NPTracer Returns: - Callable: the tracing function that needs to be called to trace ufunc + Callable: the tracing function that needs to be called to trace func """ - tracing_func = NPTracer.UFUNC_ROUTING.get(ufunc, None) + tracing_func: Optional[Callable] + if isinstance(func, numpy.ufunc): + tracing_func = NPTracer.UFUNC_ROUTING.get(func, None) + else: + tracing_func = NPTracer.FUNC_ROUTING.get(func, None) + if tracing_func is None: raise NotImplementedError( - f"NPTracer does not yet manage the following ufunc: {ufunc.__name__}" + f"NPTracer does not yet manage the following func: {func.__name__}" ) return tracing_func @@ -105,8 +121,8 @@ class NPTracer(BaseTracer): return self.__class__([], NPConstant(constant_data), 0) @staticmethod - def _manage_dtypes(ufunc: numpy.ufunc, *input_tracers: "NPTracer"): - output_dtypes = get_ufunc_numpy_output_dtype( + def _manage_dtypes(ufunc: Union[numpy.ufunc, Callable], *input_tracers: BaseTracer): + output_dtypes = get_numpy_function_output_dtype( ufunc, [input_tracer.output.data_type for input_tracer in input_tracers] ) common_output_dtypes = [ @@ -132,7 +148,9 @@ class NPTracer(BaseTracer): op_name="np.rint", ) output_tracer = self.__class__( - input_tracers, traced_computation=traced_computation, output_index=0 + input_tracers, + traced_computation=traced_computation, + output_index=0, ) return output_tracer @@ -154,7 +172,34 @@ class NPTracer(BaseTracer): op_name="np.sin", ) output_tracer = self.__class__( - input_tracers, traced_computation=traced_computation, output_index=0 + input_tracers, + traced_computation=traced_computation, + output_index=0, + ) + return output_tracer + + def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": + """Function to trace numpy.dot. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + # input_tracers contains the other tracer of the dot product + dot_inputs = (self, self._sanitize(other_tracer)) + + common_output_dtypes = self._manage_dtypes(numpy.dot, *dot_inputs) + assert len(common_output_dtypes) == 1 + + traced_computation = Dot( + [input_tracer.output for input_tracer in dot_inputs], + common_output_dtypes[0], + delegate_evaluation_function=numpy.dot, + ) + + output_tracer = self.__class__( + dot_inputs, + traced_computation=traced_computation, + output_index=0, ) return output_tracer @@ -163,6 +208,10 @@ class NPTracer(BaseTracer): numpy.sin: sin, } + FUNC_ROUTING: Dict[Callable, Callable] = { + numpy.dot: dot, + } + def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 65679d228..5ef0aaed0 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -1,11 +1,12 @@ """Test file for hnumpy debugging functions""" +import numpy import pytest from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable -from hdk.common.values import ClearValue, EncryptedValue +from hdk.common.values import ClearValue, EncryptedTensor, EncryptedValue from hdk.hnumpy import tracing LOOKUP_TABLE_FROM_2B_TO_4B = LookupTable([9, 2, 4, 11]) @@ -178,6 +179,36 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph ) +@pytest.mark.parametrize( + "lambda_f,params,ref_graph_str", + [ + # pylint: disable=unnecessary-lambda + ( + lambda x, y: numpy.dot(x, y), + { + "x": EncryptedTensor(Integer(2, is_signed=False), shape=(3,)), + "y": EncryptedTensor(Integer(2, is_signed=False), shape=(3,)), + }, + "\n%0 = x\n%1 = y\n%2 = Dot(0, 1)\nreturn(%2)", + ), + # pylint: enable=unnecessary-lambda + ], +) +def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): + "Test hnumpy get_printable_graph and draw_graph on graphs with dot" + graph = tracing.trace_numpy_function(lambda_f, params) + + draw_graph(graph, show=False) + + str_of_the_graph = get_printable_graph(graph) + + assert str_of_the_graph == ref_graph_str, ( + f"\n==================\nGot {str_of_the_graph}" + f"\n==================\nExpected {ref_graph_str}" + f"\n==================\n" + ) + + # Remark that the bitwidths are not particularly correct (eg, a MUL of a 17b times 23b # returning 23b), since they are replaced later by the real bitwidths computed on the # dataset diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 529b38fd5..338591d71 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -291,10 +291,64 @@ def test_trace_hnumpy_supported_ufuncs( @pytest.mark.parametrize( - "np_ufunc,expected_tracing_func", + "function_to_trace,inputs,expected_output_node,expected_output_value", + [ + # pylint: disable=unnecessary-lambda + pytest.param( + lambda x, y: numpy.dot(x, y), + { + "x": EncryptedTensor(Integer(7, is_signed=False), shape=(10,)), + "y": EncryptedTensor(Integer(7, is_signed=False), shape=(10,)), + }, + ir.Dot, + EncryptedValue(Integer(32, False)), + ), + pytest.param( + lambda x, y: numpy.dot(x, y), + { + "x": EncryptedTensor(Float(64), shape=(42,)), + "y": EncryptedTensor(Float(64), shape=(10,)), + }, + ir.Dot, + EncryptedValue(Float(64)), + ), + pytest.param( + lambda x, y: numpy.dot(x, y), + { + "x": ClearTensor(Integer(64, is_signed=True), shape=(6,)), + "y": ClearTensor(Integer(64, is_signed=True), shape=(6,)), + }, + ir.Dot, + ClearValue(Integer(64, is_signed=True)), + ), + pytest.param( + lambda x: numpy.dot(x, numpy.array([1, 2, 3, 4, 5], dtype=numpy.int64)), + { + "x": EncryptedTensor(Integer(64, is_signed=True), shape=(5,)), + }, + ir.Dot, + EncryptedValue(Integer(64, True)), + ), + # pylint: enable=unnecessary-lambda + ], +) +def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expected_output_value): + """Function to test dot tracing""" + + op_graph = tracing.trace_numpy_function(function_to_trace, inputs) + + assert len(op_graph.output_nodes) == 1 + assert isinstance(op_graph.output_nodes[0], expected_output_node) + assert len(op_graph.output_nodes[0].outputs) == 1 + assert op_graph.output_nodes[0].outputs[0] == expected_output_value + + +@pytest.mark.parametrize( + "np_function,expected_tracing_func", [ pytest.param(numpy.rint, tracing.NPTracer.rint), pytest.param(numpy.sin, tracing.NPTracer.sin), + pytest.param(numpy.dot, tracing.NPTracer.dot), # There is a need to test the case where the function fails, I chose numpy.conjugate which # works on complex types, as we don't talk about complex types for now this looks like a # good long term candidate to check for an unsupported function @@ -303,9 +357,9 @@ def test_trace_hnumpy_supported_ufuncs( ), ], ) -def test_nptracer_get_tracing_func_for_np_ufunc(np_ufunc, expected_tracing_func): - """Test NPTracer get_tracing_func_for_np_ufunc""" - assert tracing.NPTracer.get_tracing_func_for_np_ufunc(np_ufunc) == expected_tracing_func +def test_nptracer_get_tracing_func_for_np_functions(np_function, expected_tracing_func): + """Test NPTracer get_tracing_func_for_np_function""" + assert tracing.NPTracer.get_tracing_func_for_np_function(np_function) == expected_tracing_func @pytest.mark.parametrize( From 202dffb4a5d8ea57ff41bf246948c45521210572 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 25 Aug 2021 14:09:54 +0200 Subject: [PATCH 0139/1104] Build/env dev docker 127 (#196) build: add workflow to create HDK env Docker Image - make bot username a secret - update Makefile to pull the HDK image - have a separate Makefile for the dev specific Docker Image needs --- .github/workflows/continuous-integration.yaml | 12 ++--- .github/workflows/docker-env.yaml | 54 +++++++++++++++++++ Makefile | 9 ++-- docker/{Dockerfile => Dockerfile.hdk-dev} | 8 +-- docker/Dockerfile.hdk-env | 10 ++++ 5 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/docker-env.yaml rename docker/{Dockerfile => Dockerfile.hdk-dev} (59%) create mode 100644 docker/Dockerfile.hdk-env diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index d2931efe7..07e1d1833 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -13,19 +13,15 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/zama-ai/zamalang-compiler + image: ghcr.io/zama-ai/hdk-env credentials: - username: zama-bot + username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} strategy: matrix: python-version: [3.8] steps: - - name: Install Git - run: apt-get install git -y - - name: Install Graphviz - run: apt-get install graphviz* -y - name: Checkout Code uses: actions/checkout@v2 with: @@ -109,7 +105,7 @@ jobs: SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: 'Build finished with status ${{ job.status }}' - SLACK_USERNAME: zama-bot + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} publish-docs: @@ -156,5 +152,5 @@ jobs: SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: 'Publishing documentation finished with status ${{ job.status }}' - SLACK_USERNAME: zama-bot + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml new file mode 100644 index 000000000..2b76c14e1 --- /dev/null +++ b/.github/workflows/docker-env.yaml @@ -0,0 +1,54 @@ +name: Docker image (HDK dev/CI) + +on: + push: + branches: + - main + paths: + - docker/Dockerfile.hdk-env + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build_publish: + name: Build & Push the HDK env Docker Image + runs-on: ubuntu-20.04 + env: + IMAGE_URL: ghcr.io/zama-ai/hdk-env + + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + - name: Build hdk-env Image + if: ${{ success() && !cancelled() }} + uses: docker/build-push-action@v2 + with: + context: . + builder: ${{ steps.buildx.outputs.name }} + file: docker/Dockerfile.hdk-env + push: true + tags: "${{ env.IMAGE_URL }}:latest" + no-cache: true + + - name: Slack Notification + if: ${{ always() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: hdk-updates + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Publishing Docker Image ${{ env.IMAGE_URL }} \ + finished with status ${{ job.status }}" + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + diff --git a/Makefile b/Makefile index 8046b9cf7..25741b9d2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ SHELL:=/bin/bash +DEV_DOCKER_IMG:=hdk:dev +DEV_DOCKERFILE:=docker/Dockerfile.hdk-dev + setup_env: poetry install poetry run python -m pip install -U pip wheel setuptools @@ -99,16 +102,16 @@ coverage: .PHONY: coverage docker_build: - docker build -t hdk:mlir -f docker/Dockerfile . + docker build -t $(DEV_DOCKER_IMG) -f $(DEV_DOCKERFILE) . .PHONY: docker_build docker_rebuild: - docker build --no-cache -t hdk:mlir -f docker/Dockerfile . + docker build --no-cache -t $(DEV_DOCKER_IMG) -f $(DEV_DOCKERFILE) . .PHONY: docker_rebuild docker_start: @# the slash before pwd is for Windows - docker run --rm -it -p 8888:8888 --volume /"$$(pwd)":/hdk hdk:mlir + docker run --rm -it -p 8888:8888 --volume /"$$(pwd)":/hdk $(DEV_DOCKER_IMG) .PHONY: docker_start docker_build_and_start: docker_build docker_start diff --git a/docker/Dockerfile b/docker/Dockerfile.hdk-dev similarity index 59% rename from docker/Dockerfile rename to docker/Dockerfile.hdk-dev index b3db5a053..70d88956e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile.hdk-dev @@ -1,10 +1,6 @@ -FROM ghcr.io/zama-ai/zamalang-compiler +FROM ghcr.io/zama-ai/hdk-env -RUN apt-get install --no-install-recommends -y \ - python3.8 python3.8-venv python-is-python3 git graphviz* && \ - pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir poetry && \ - echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ +RUN echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ echo "if [[ \"\$?\" != \"0\" ]]; then" >> /root/.bashrc && \ echo " python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ echo " source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ diff --git a/docker/Dockerfile.hdk-env b/docker/Dockerfile.hdk-env new file mode 100644 index 000000000..26bf5eea4 --- /dev/null +++ b/docker/Dockerfile.hdk-env @@ -0,0 +1,10 @@ +FROM ghcr.io/zama-ai/zamalang-compiler + +RUN apt-get install --no-install-recommends -y \ + python3.8 \ + python3.8-venv \ + python-is-python3 \ + git \ + graphviz* && \ + pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir poetry From 0f7f5a302a4e234ac95fba79bfa859622ab5f05a Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Tue, 17 Aug 2021 18:17:43 +0200 Subject: [PATCH 0140/1104] chore(templates): update issue templates refers #129 --- .github/ISSUE_TEMPLATE/bug_report.md | 8 -------- .github/ISSUE_TEMPLATE/refactor.md | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 49d8c45a9..0661904c6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,14 +30,6 @@ print("Minimal POC to reproduce the bug") ## Artifacts -Generate a compiler report (see documentation) and attach all generated artifacts here: - -- bounds.txt -- cryptographic_parameters.txt -- ir_nodes.txt -- optimizations_applied.txt -- target_nodes.txt -
Logs or output

diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md index b5023e9f0..dc8d53030 100644 --- a/.github/ISSUE_TEMPLATE/refactor.md +++ b/.github/ISSUE_TEMPLATE/refactor.md @@ -6,11 +6,11 @@ labels: refactor ## Proposal -What would be your refactor proposal +What would be your refactor proposal? ## Impact -List all files/modules/projects impacted by this refactor +List all files/modules/projects impacted by this refactor.

Files impacted

From 1ebbd3ea9123ce2309607cdeb22f628807e3910f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 25 Aug 2021 15:45:10 +0200 Subject: [PATCH 0141/1104] fix(build): only allow one Docker image build per ref/branch --- .github/workflows/docker-env.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index 2b76c14e1..696e23f0b 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -12,6 +12,10 @@ on: jobs: build_publish: + concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + name: Build & Push the HDK env Docker Image runs-on: ubuntu-20.04 env: From 9a3e15e89ab6d99ee1ade0c86b103adee79b7a59 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 25 Aug 2021 15:58:23 +0200 Subject: [PATCH 0142/1104] test: add tests of np.dot with compile_numpy_function_into_op_graph closes #201 --- tests/hnumpy/test_compile.py | 59 +++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 353bd7ec7..0a62b19a0 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -8,7 +8,7 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedTensor, EncryptedValue from hdk.hnumpy.compile import ( compile_numpy_function, compile_numpy_function_into_op_graph, @@ -43,7 +43,7 @@ def no_fuse_unhandled(x, y): ], ) def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_names): - """Test function compile_numpy_function for a program with multiple outputs""" + """Test function compile_numpy_function_into_op_graph for a program with multiple outputs""" def data_gen(args): for prod in itertools.product(*args): @@ -131,7 +131,7 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): def test_compile_function_with_direct_tlu(): - """Test compile_numpy_function for a program with direct table lookup""" + """Test compile_numpy_function_into_op_graph for a program with direct table lookup""" table = LookupTable([9, 2, 4, 11]) @@ -149,7 +149,7 @@ def test_compile_function_with_direct_tlu(): def test_compile_function_with_direct_tlu_overflow(): - """Test compile_numpy_function for a program with direct table lookup overflow""" + """Test compile_numpy_function_into_op_graph for a program with direct table lookup overflow""" table = LookupTable([9, 2, 4, 11]) @@ -171,7 +171,7 @@ def test_compile_function_with_direct_tlu_overflow(): ], ) def test_fail_compile(function, input_ranges, list_of_arg_names): - """Test function compile_numpy_function for a program with signed values""" + """Test function compile_numpy_function_into_op_graph for a program with signed values""" def data_gen(args): for prod in itertools.product(*args): @@ -187,3 +187,52 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), ) + + +@pytest.mark.parametrize( + "function,params,shape,ref_graph_str", + [ + # pylint: disable=unnecessary-lambda + ( + lambda x, y: numpy.dot(x, y), + { + "x": EncryptedTensor(Integer(2, is_signed=False), shape=(4,)), + "y": EncryptedTensor(Integer(2, is_signed=False), shape=(4,)), + }, + (4,), + # Remark that, when you do the dot of tensors of 4 values between 0 and 3, + # you can get a maximal value of 4*3*3 = 36, ie something on 6 bits + "\n%0 = x # Integer" + "\n%1 = y # Integer" + "\n%2 = Dot(0, 1) # Integer" + "\nreturn(%2)", + ), + # pylint: enable=unnecessary-lambda + ], +) +def test_compile_function_with_dot(function, params, shape, ref_graph_str): + """Test compile_numpy_function_into_op_graph for a program with np.dot""" + + # This is the exhaust, but if ever we have too long inputs (ie, large 'repeat'), + # we'll have to take random values, not all values one by one + def data_gen(max_for_ij, repeat): + iter_i = itertools.product(range(0, max_for_ij + 1), repeat=repeat) + iter_j = itertools.product(range(0, max_for_ij + 1), repeat=repeat) + for prod_i, prod_j in itertools.product(iter_i, iter_j): + yield (prod_i, prod_j) + + max_for_ij = 3 + assert len(shape) == 1 + repeat = shape[0] + + op_graph = compile_numpy_function_into_op_graph( + function, + params, + data_gen(max_for_ij, repeat), + ) + str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) + assert str_of_the_graph == ref_graph_str, ( + f"\n==================\nGot {str_of_the_graph}" + f"\n==================\nExpected {ref_graph_str}" + f"\n==================\n" + ) From 31259e556c4629241edbf87c491bce350b9ee177 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 26 Aug 2021 11:01:05 +0200 Subject: [PATCH 0143/1104] refactor: remove the content= when adding nodes to a graph - required by tests but can be done by the testing function itself --- hdk/common/optimization/topological.py | 2 +- hdk/common/tracing/tracing_helpers.py | 4 ++-- tests/common/extensions/test_table.py | 12 ++++++------ tests/conftest.py | 10 +++++++++- tests/helpers/test_conftest.py | 16 ---------------- tests/hnumpy/test_tracing.py | 8 ++++---- 6 files changed, 22 insertions(+), 30 deletions(-) diff --git a/hdk/common/optimization/topological.py b/hdk/common/optimization/topological.py index 8b5457127..e374ca57e 100644 --- a/hdk/common/optimization/topological.py +++ b/hdk/common/optimization/topological.py @@ -42,7 +42,7 @@ def fuse_float_operations(op_graph: OPGraph): fused_node, node_before_subgraph = subgraph_conversion_result - nx_graph.add_node(fused_node, content=fused_node) + nx_graph.add_node(fused_node) if terminal_node in op_graph.output_nodes.values(): # Output value replace it diff --git a/hdk/common/tracing/tracing_helpers.py b/hdk/common/tracing/tracing_helpers.py index 0e643a894..34043bda7 100644 --- a/hdk/common/tracing/tracing_helpers.py +++ b/hdk/common/tracing/tracing_helpers.py @@ -108,11 +108,11 @@ def create_graph_from_output_tracers( next_tracers: Dict[BaseTracer, None] = dict() for tracer in current_tracers: current_ir_node = tracer.traced_computation - graph.add_node(current_ir_node, content=current_ir_node) + graph.add_node(current_ir_node) for input_idx, input_tracer in enumerate(tracer.inputs): input_ir_node = input_tracer.traced_computation - graph.add_node(input_ir_node, content=input_ir_node) + graph.add_node(input_ir_node) graph.add_edge(input_ir_node, current_ir_node, input_idx=input_idx) if input_tracer not in visited_tracers: next_tracers.update({input_tracer: None}) diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index bc5658d7b..f936367d1 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -50,7 +50,7 @@ def test_lookup_table_encrypted_lookup(test_helpers): # (x) - (TLU) input_x = ir.Input(input_value=x, input_name="x", program_input_idx=0) - ref_graph.add_node(input_x, content=input_x) + ref_graph.add_node(input_x) output_arbitrary_function = ir.ArbitraryFunction( input_base_value=x, @@ -59,7 +59,7 @@ def test_lookup_table_encrypted_lookup(test_helpers): op_kwargs={"table": deepcopy(table.table)}, op_name="TLU", ) - ref_graph.add_node(output_arbitrary_function, content=output_arbitrary_function) + ref_graph.add_node(output_arbitrary_function) ref_graph.add_edge(input_x, output_arbitrary_function, input_idx=0) @@ -87,7 +87,7 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): # (3) input_x = ir.Input(input_value=x, input_name="x", program_input_idx=0) - ref_graph.add_node(input_x, content=input_x) + ref_graph.add_node(input_x) intermediate_arbitrary_function = ir.ArbitraryFunction( input_base_value=x, @@ -96,13 +96,13 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): op_kwargs={"table": deepcopy(table.table)}, op_name="TLU", ) - ref_graph.add_node(intermediate_arbitrary_function, content=intermediate_arbitrary_function) + ref_graph.add_node(intermediate_arbitrary_function) constant_3 = ir.Constant(3) - ref_graph.add_node(constant_3, content=constant_3) + ref_graph.add_node(constant_3) output_add = ir.Add((intermediate_arbitrary_function.outputs[0], constant_3.outputs[0])) - ref_graph.add_node(output_add, content=output_add) + ref_graph.add_node(output_add) ref_graph.add_edge(input_x, intermediate_arbitrary_function, input_idx=0) diff --git a/tests/conftest.py b/tests/conftest.py index 93206f8f4..f222bd3fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,8 +13,16 @@ class TestHelpers: # edge_match is a copy of node_match edge_matcher = iso.categorical_multiedge_match("input_idx", None) node_matcher = iso.generic_node_match( - "content", None, lambda lhs, rhs: lhs.is_equivalent_to(rhs) + "_test_content", None, lambda lhs, rhs: lhs.is_equivalent_to(rhs) ) + + # Set the _test_content for each node in the graphs + for node in reference.nodes(): + reference.add_node(node, _test_content=node) + + for node in to_compare.nodes(): + to_compare.add_node(node, _test_content=node) + graphs_are_isomorphic = nx.is_isomorphic( reference, to_compare, diff --git a/tests/helpers/test_conftest.py b/tests/helpers/test_conftest.py index 50b8f7d53..9f5a50a29 100644 --- a/tests/helpers/test_conftest.py +++ b/tests/helpers/test_conftest.py @@ -31,10 +31,6 @@ def test_digraphs_are_equivalent(test_helpers): g_1.add_edge(t_0, t_2, input_idx=0) g_1.add_edge(t_1, t_2, input_idx=1) - # This updates the nodes attributes in the graph - for node in g_1: - g_1.add_node(node, content=node) - t0p = TestNode("Add") t1p = TestNode("Mul") t2p = TestNode("TLU") @@ -42,10 +38,6 @@ def test_digraphs_are_equivalent(test_helpers): g_2.add_edge(t1p, t2p, input_idx=1) g_2.add_edge(t0p, t2p, input_idx=0) - # This updates the nodes attributes in the graph - for node in g_2: - g_2.add_node(node, content=node) - bad_g2 = nx.MultiDiGraph() bad_t0 = TestNode("Not Add") @@ -53,19 +45,11 @@ def test_digraphs_are_equivalent(test_helpers): bad_g2.add_edge(bad_t0, t_2, input_idx=0) bad_g2.add_edge(t_1, t_2, input_idx=1) - # This updates the nodes attributes in the graph - for node in bad_g2: - bad_g2.add_node(node, content=node) - bad_g3 = nx.MultiDiGraph() bad_g3.add_edge(t_0, t_2, input_idx=1) bad_g3.add_edge(t_1, t_2, input_idx=0) - # This updates the nodes attributes in the graph - for node in bad_g3: - bad_g3.add_node(node, content=node) - assert test_helpers.digraphs_are_equivalent(g_1, g_2), "Graphs should be equivalent" assert not test_helpers.digraphs_are_equivalent(g_1, bad_g2), "Graphs should not be equivalent" assert not test_helpers.digraphs_are_equivalent(g_2, bad_g2), "Graphs should not be equivalent" diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 338591d71..5e8a76ff6 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -100,10 +100,10 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): ) ) - ref_graph.add_node(input_x, content=input_x) - ref_graph.add_node(input_y, content=input_y) - ref_graph.add_node(add_node_z, content=add_node_z) - ref_graph.add_node(returned_final_node, content=returned_final_node) + ref_graph.add_node(input_x) + ref_graph.add_node(input_y) + ref_graph.add_node(add_node_z) + ref_graph.add_node(returned_final_node) ref_graph.add_edge(input_x, add_node_z, input_idx=0) ref_graph.add_edge(input_x, add_node_z, input_idx=1) From 1b489b281cfe14a4d7fe495bbc8db45a4fffd04e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 26 Aug 2021 11:17:56 +0200 Subject: [PATCH 0144/1104] refactor: move is_equivalent to conftest.py for tests --- hdk/common/representation/intermediate.py | 71 ---------- .../representation/test_intermediate.py | 7 +- tests/conftest.py | 125 +++++++++++++++++- 3 files changed, 130 insertions(+), 73 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index d0cfef740..bbe6530c5 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -49,41 +49,6 @@ class IntermediateNode(ABC): self.outputs = [mix_values_func(self.inputs[0], self.inputs[1])] - def _is_equivalent_to_binary_commutative(self, other: object) -> bool: - """is_equivalent_to for a binary and commutative operation.""" - return ( - isinstance(other, self.__class__) - and (self.inputs == other.inputs or self.inputs == other.inputs[::-1]) - and self.outputs == other.outputs - ) - - def _is_equivalent_to_binary_non_commutative(self, other: object) -> bool: - """is_equivalent_to for a binary and non-commutative operation.""" - return ( - isinstance(other, self.__class__) - and self.inputs == other.inputs - and self.outputs == other.outputs - ) - - @abstractmethod - def is_equivalent_to(self, other: object) -> bool: - """Alternative to __eq__ to check equivalence between IntermediateNodes. - - Overriding __eq__ has unwanted side effects, this provides the same facility without - disrupting expected behavior too much - - Args: - other (object): Other object to check against - - Returns: - bool: True if the other object is equivalent - """ - return ( - isinstance(other, IntermediateNode) - and self.inputs == other.inputs - and self.outputs == other.outputs - ) - @abstractmethod def evaluate(self, inputs: Dict[int, Any]) -> Any: """Function to simulate what the represented computation would output for the given inputs. @@ -129,7 +94,6 @@ class Add(IntermediateNode): _n_in: int = 2 __init__ = IntermediateNode._init_binary - is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] + inputs[1] @@ -144,7 +108,6 @@ class Sub(IntermediateNode): _n_in: int = 2 __init__ = IntermediateNode._init_binary - is_equivalent_to = IntermediateNode._is_equivalent_to_binary_non_commutative def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] - inputs[1] @@ -159,7 +122,6 @@ class Mul(IntermediateNode): _n_in: int = 2 __init__ = IntermediateNode._init_binary - is_equivalent_to = IntermediateNode._is_equivalent_to_binary_commutative def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] * inputs[1] @@ -190,14 +152,6 @@ class Input(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return inputs[0] - def is_equivalent_to(self, other: object) -> bool: - return ( - isinstance(other, Input) - and self.input_name == other.input_name - and self.program_input_idx == other.program_input_idx - and super().is_equivalent_to(other) - ) - def label(self) -> str: return self.input_name @@ -225,13 +179,6 @@ class Constant(IntermediateNode): def evaluate(self, inputs: Dict[int, Any]) -> Any: return self.constant_data - def is_equivalent_to(self, other: object) -> bool: - return ( - isinstance(other, Constant) - and self.constant_data == other.constant_data - and super().is_equivalent_to(other) - ) - @property def constant_data(self) -> Any: """Returns the constant_data stored in the Constant node. @@ -278,17 +225,6 @@ class ArbitraryFunction(IntermediateNode): assert self.arbitrary_func is not None return self.arbitrary_func(inputs[0], *self.op_args, **self.op_kwargs) - def is_equivalent_to(self, other: object) -> bool: - # FIXME: comparing self.arbitrary_func to other.arbitrary_func will not work - # Only evaluating over the same set of inputs and comparing will help - return ( - isinstance(other, ArbitraryFunction) - and self.op_args == other.op_args - and self.op_kwargs == other.op_kwargs - and self.op_name == other.op_name - and super().is_equivalent_to(other) - ) - def label(self) -> str: return self.op_name @@ -344,12 +280,5 @@ class Dot(IntermediateNode): assert self.evaluation_function is not None return self.evaluation_function(inputs[0], inputs[1]) - def is_equivalent_to(self, other: object) -> bool: - return ( - isinstance(other, self.__class__) - and self.evaluation_function == other.evaluation_function - and super().is_equivalent_to(other) - ) - def label(self) -> str: return "dot" diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 9283fabd0..4b08d655c 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -276,6 +276,11 @@ def test_is_equivalent_to( node1: ir.IntermediateNode, node2: ir.IntermediateNode, expected_result: bool, + test_helpers, ): """Test is_equivalent_to methods on IntermediateNodes""" - assert node1.is_equivalent_to(node2) == node2.is_equivalent_to(node1) == expected_result + assert ( + test_helpers.nodes_are_equivalent(node1, node2) + == test_helpers.nodes_are_equivalent(node2, node1) + == expected_result + ) diff --git a/tests/conftest.py b/tests/conftest.py index f222bd3fa..9454fb98f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,142 @@ """PyTest configuration file""" +from typing import Callable, Dict, Type + import networkx as nx import networkx.algorithms.isomorphism as iso import pytest +from hdk.common.representation.intermediate import ( + ALL_IR_NODES, + Add, + ArbitraryFunction, + Constant, + Dot, + Input, + IntermediateNode, + Mul, + Sub, +) + + +def _is_equivalent_to_binary_commutative(lhs: IntermediateNode, rhs: object) -> bool: + """is_equivalent_to for a binary and commutative operation.""" + return ( + isinstance(rhs, lhs.__class__) + and (lhs.inputs == rhs.inputs or lhs.inputs == rhs.inputs[::-1]) + and lhs.outputs == rhs.outputs + ) + + +def _is_equivalent_to_binary_non_commutative(lhs: IntermediateNode, rhs: object) -> bool: + """is_equivalent_to for a binary and non-commutative operation.""" + return ( + isinstance(rhs, lhs.__class__) and lhs.inputs == rhs.inputs and lhs.outputs == rhs.outputs + ) + + +def is_equivalent_add(lhs: Add, rhs: object) -> bool: + """Helper function to check if an Add node is equivalent to an other object.""" + return _is_equivalent_to_binary_commutative(lhs, rhs) + + +def is_equivalent_arbitrary_function(lhs: ArbitraryFunction, rhs: object) -> bool: + """Helper function to check if an ArbitraryFunction node is equivalent to an other object.""" + return ( + isinstance(rhs, ArbitraryFunction) + and lhs.op_args == rhs.op_args + and lhs.op_kwargs == rhs.op_kwargs + and lhs.op_name == rhs.op_name + and is_equivalent_intermediate_node(lhs, rhs) + ) + + +def is_equivalent_constant(lhs: Constant, rhs: object) -> bool: + """Helper function to check if a Constant node is equivalent to an other object.""" + return ( + isinstance(rhs, Constant) + and lhs.constant_data == rhs.constant_data + and is_equivalent_intermediate_node(lhs, rhs) + ) + + +def is_equivalent_dot(lhs: Dot, rhs: object) -> bool: + """Helper function to check if a Dot node is equivalent to an other object.""" + return ( + isinstance(rhs, Dot) + and lhs.evaluation_function == rhs.evaluation_function + and is_equivalent_intermediate_node(lhs, rhs) + ) + + +def is_equivalent_input(lhs: Input, rhs: object) -> bool: + """Helper function to check if an Input node is equivalent to an other object.""" + return ( + isinstance(rhs, Input) + and lhs.input_name == rhs.input_name + and lhs.program_input_idx == rhs.program_input_idx + and is_equivalent_intermediate_node(lhs, rhs) + ) + + +def is_equivalent_mul(lhs: Mul, rhs: object) -> bool: + """Helper function to check if a Mul node is equivalent to an other object.""" + return _is_equivalent_to_binary_commutative(lhs, rhs) + + +def is_equivalent_sub(lhs: Sub, rhs: object) -> bool: + """Helper function to check if a Sub node is equivalent to an other object.""" + return _is_equivalent_to_binary_non_commutative(lhs, rhs) + + +def is_equivalent_intermediate_node(lhs: IntermediateNode, rhs: object) -> bool: + """Helper function to check if an IntermediateNode node is equivalent to an other object.""" + return ( + isinstance(rhs, IntermediateNode) + and lhs.inputs == rhs.inputs + and lhs.outputs == rhs.outputs + ) + + +EQUIVALENT_TEST_FUNC: Dict[Type, Callable[..., bool]] = { + Add: is_equivalent_add, + ArbitraryFunction: is_equivalent_arbitrary_function, + Constant: is_equivalent_constant, + Dot: is_equivalent_dot, + Input: is_equivalent_input, + Mul: is_equivalent_mul, + Sub: is_equivalent_sub, +} + +_missing_nodes_in_mapping = ALL_IR_NODES - EQUIVALENT_TEST_FUNC.keys() +assert len(_missing_nodes_in_mapping) == 0, ( + f"Missing IR node in EQUIVALENT_TEST_FUNC : " + f"{', '.join(sorted(str(node_type) for node_type in _missing_nodes_in_mapping))}" +) + +del _missing_nodes_in_mapping + class TestHelpers: """Class allowing to pass helper functions to tests""" + @staticmethod + def nodes_are_equivalent(lhs, rhs) -> bool: + """Helper function for tests to check if two nodes are equivalent.""" + equivalent_func = EQUIVALENT_TEST_FUNC.get(type(lhs), None) + if equivalent_func is not None: + return equivalent_func(lhs, rhs) + + # This is a default for the test_conftest.py that should remain separate from the package + # nodes is_equivalent_* functions + return lhs.is_equivalent_to(rhs) + @staticmethod def digraphs_are_equivalent(reference: nx.MultiDiGraph, to_compare: nx.MultiDiGraph): """Check that two digraphs are equivalent without modifications""" # edge_match is a copy of node_match edge_matcher = iso.categorical_multiedge_match("input_idx", None) node_matcher = iso.generic_node_match( - "_test_content", None, lambda lhs, rhs: lhs.is_equivalent_to(rhs) + "_test_content", None, TestHelpers.nodes_are_equivalent ) # Set the _test_content for each node in the graphs From 3541e4ff4e639520124e2fc20851d557a2b8caf4 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 25 Aug 2021 18:24:24 +0200 Subject: [PATCH 0145/1104] fix(tools): various Makefile improvements - sync output recursively for make calls - add a script to get the number of cpus on mac and linux - Makefile formatting - update serialize_targets.sh to invoke the proper make binary - Add instructions to install make --- Makefile | 18 ++++++++++++------ docs/dev/GETTING-STARTED.md | 23 +++++++++++++++++++++++ script/make_utils/ncpus.sh | 7 +++++++ script/make_utils/serialize_targets.sh | 6 +++++- 4 files changed, 47 insertions(+), 7 deletions(-) create mode 100755 script/make_utils/ncpus.sh diff --git a/Makefile b/Makefile index 25741b9d2..0985de3b4 100644 --- a/Makefile +++ b/Makefile @@ -16,11 +16,13 @@ sync_env: .PHONY: sync_env python_format: - poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --dir benchmarks + poetry run env bash ./script/source_format/format_python.sh \ + --dir hdk --dir tests --dir benchmarks .PHONY: python_format check_python_format: - poetry run env bash ./script/source_format/format_python.sh --dir hdk --dir tests --dir benchmarks --check + poetry run env bash ./script/source_format/format_python.sh \ + --dir hdk --dir tests --dir benchmarks --check .PHONY: check_python_format check_strip_nb: @@ -28,7 +30,8 @@ check_strip_nb: .PHONY: strip_nb pylint: - +poetry run env bash script/make_utils/serialize_targets.sh pylint_src pylint_tests pylint_benchmarks + +poetry run env bash script/make_utils/serialize_targets.sh $(MAKE) \ + pylint_src pylint_tests pylint_benchmarks .PHONY: pylint pylint_src: @@ -46,7 +49,8 @@ pylint_benchmarks: .PHONY: pylint_benchmarks flake8: - poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" hdk/ tests/ benchmarks/ + poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" \ + hdk/ tests/ benchmarks/ .PHONY: flake8 python_linting: pylint flake8 @@ -56,7 +60,8 @@ conformance: strip_nb python_format .PHONY: conformance pcc: - @$(MAKE) --keep-going --jobs $$(nproc) --output-sync --no-print-directory pcc_internal + @$(MAKE) --keep-going --jobs $(./script/make_utils/ncpus.sh) --output-sync=recurse \ + --no-print-directory pcc_internal .PHONY: pcc pcc_internal: check_python_format check_strip_nb python_linting mypy_ci pydocstyle @@ -89,7 +94,8 @@ mypy_benchmark: # the parent make execution. We serialize calls to these targets as they may overwrite each others # cache which can cause issues. mypy_ci: - +poetry run env bash script/make_utils/serialize_targets.sh mypy mypy_test mypy_benchmark + +poetry run env bash script/make_utils/serialize_targets.sh $(MAKE) \ + mypy mypy_test mypy_benchmark .PHONY: mypy_ci pytest_and_coverage: pytest coverage diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 2c8577131..8c05213f9 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -16,6 +16,29 @@ You can follow [this](https://realpython.com/installing-python/) guide to instal You can follow [this](https://python-poetry.org/docs/#installation) official guide to install it. +### Installing make + +The dev tools use make to launch the various commands. + +On Linux you can install make from your distribution's preferred package manager. + +On Mac OS you can install a more recent version of make via brew: + +```consol +# check for gmake +which gmake +# If you don't have it, it will error out, install gmake +brew install make +# recheck, now you should have gmake +which gmake +``` + +It is possible to install gmake as make, check this [StackOverflow post](https://stackoverflow.com/questions/38901894/how-can-i-install-a-newer-version-of-make-on-mac-os) for more infos. + +On Windows check [this GitHub gist](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make). + +**/!\\ In the next sections, be sure to use the proper `make` tool for your system, `make`, `gmake` or other. /!\\** + ### Cloning repository Now, it's time to get the source code of `hdk`. You can use the following command to do that. diff --git a/script/make_utils/ncpus.sh b/script/make_utils/ncpus.sh new file mode 100755 index 000000000..2c8cfb618 --- /dev/null +++ b/script/make_utils/ncpus.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [[ `uname` == "Darwin" ]]; then + sysctl -n hw.logicalcpu +else + nproc +fi diff --git a/script/make_utils/serialize_targets.sh b/script/make_utils/serialize_targets.sh index 2b4b802af..c018ad80f 100755 --- a/script/make_utils/serialize_targets.sh +++ b/script/make_utils/serialize_targets.sh @@ -4,8 +4,12 @@ set +e EXIT_CODE=0 +# Get the make executable +MAKE="$1" +shift + for make_target in "$@"; do - make --no-print-directory "${make_target}" + "${MAKE}" --no-print-directory "${make_target}" if [[ "$?" != "0" ]]; then EXIT_CODE=1 fi From 61daa49e9d964265ef2e470ad6c0784381e3f54d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 26 Aug 2021 14:37:35 +0200 Subject: [PATCH 0146/1104] fix(tools): update Makefile targets to allow make binary path with spaces - avoids issues if the make binary path contains spaces --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 0985de3b4..56b60c8c1 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ check_strip_nb: .PHONY: strip_nb pylint: - +poetry run env bash script/make_utils/serialize_targets.sh $(MAKE) \ + +poetry run env bash script/make_utils/serialize_targets.sh "$(MAKE)" \ pylint_src pylint_tests pylint_benchmarks .PHONY: pylint @@ -94,7 +94,7 @@ mypy_benchmark: # the parent make execution. We serialize calls to these targets as they may overwrite each others # cache which can cause issues. mypy_ci: - +poetry run env bash script/make_utils/serialize_targets.sh $(MAKE) \ + +poetry run env bash script/make_utils/serialize_targets.sh "$(MAKE)" \ mypy mypy_test mypy_benchmark .PHONY: mypy_ci From 809ce28b3820a2d208f1679d4665dff836ecb370 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 27 Aug 2021 09:55:45 +0200 Subject: [PATCH 0147/1104] feat: an option to show MLIR closes #224 --- hdk/hnumpy/compile.py | 6 ++++++ tests/hnumpy/test_compile.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 2dbe045d4..98259e0ba 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -132,6 +132,7 @@ def compile_numpy_function( dataset: Iterator[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, + show_mlir: bool = False, ) -> CompilerEngine: """Main API of hnumpy, to be able to compile an homomorphic program. @@ -146,6 +147,8 @@ def compile_numpy_function( during compilation compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill during compilation + show_mlir (bool): if set, the MLIR produced by the converter and which is going + to be sent to the compiler backend is shown on the screen, e.g., for debugging or demo Returns: CompilerEngine: engine to run and debug the compiled graph @@ -171,6 +174,9 @@ def compile_numpy_function( converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(op_graph) + if show_mlir: + print(f"MLIR which is going to be compiled: \n{mlir_result}") + # Compile the MLIR representation engine = CompilerEngine() engine.compile_fhe(mlir_result) diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 0a62b19a0..5f04714d9 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -236,3 +236,33 @@ def test_compile_function_with_dot(function, params, shape, ref_graph_str): f"\n==================\nExpected {ref_graph_str}" f"\n==================\n" ) + + +@pytest.mark.parametrize( + "function,input_ranges,list_of_arg_names", + [ + pytest.param(lambda x: x + 64, ((0, 10),), ["x"]), + pytest.param(lambda x: x * 3, ((0, 40),), ["x"]), + pytest.param(lambda x: 120 - x, ((40, 80),), ["x"]), + pytest.param(lambda x, y: x + y + 64, ((0, 20), (0, 20)), ["x", "y"]), + pytest.param(lambda x, y: 100 - y + x, ((0, 20), (0, 20)), ["x", "y"]), + pytest.param(lambda x, y: 50 - y * 2 + x, ((0, 20), (0, 20)), ["x", "y"]), + ], +) +def test_compile_with_show_mlir(function, input_ranges, list_of_arg_names): + """Test show_mlir option""" + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedValue(Integer(64, False)) for arg_name in list_of_arg_names + } + + compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + show_mlir=True, + ) From c907cd0470c4fc2571545681eba31ded2746a4e6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 25 Aug 2021 17:56:12 +0200 Subject: [PATCH 0148/1104] docs: float fusing explanation --- docs/_static/float_fusing_example/after.png | Bin 0 -> 16617 bytes docs/_static/float_fusing_example/before.png | Bin 0 -> 60714 bytes .../_static/float_fusing_example/subgraph.png | Bin 0 -> 62691 bytes docs/dev/FLOAT-FUSING.md | 77 ++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 docs/_static/float_fusing_example/after.png create mode 100644 docs/_static/float_fusing_example/before.png create mode 100644 docs/_static/float_fusing_example/subgraph.png create mode 100644 docs/dev/FLOAT-FUSING.md diff --git a/docs/_static/float_fusing_example/after.png b/docs/_static/float_fusing_example/after.png new file mode 100644 index 0000000000000000000000000000000000000000..ddb214a4a5631df3180a470395fcf55d026e8949 GIT binary patch literal 16617 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 literal 0 HcmV?d00001 diff --git a/docs/_static/float_fusing_example/before.png b/docs/_static/float_fusing_example/before.png new file mode 100644 index 0000000000000000000000000000000000000000..94d55f981b6a6783edfe98fddb700e93b997336c GIT binary patch literal 60714 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 LNkvXXu0mjfitU={ literal 0 HcmV?d00001 diff --git a/docs/_static/float_fusing_example/subgraph.png b/docs/_static/float_fusing_example/subgraph.png new file mode 100644 index 0000000000000000000000000000000000000000..31ca8d4bc1cc843f78f3288d349ff6c32d5800c9 GIT binary patch literal 62691 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μ**G9^$?{=WRt@+m^n(G9){8LNbK%)$=jc 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 sZ*9s(|F@42#@~-Va@$l$ literal 0 HcmV?d00001 diff --git a/docs/dev/FLOAT-FUSING.md b/docs/dev/FLOAT-FUSING.md new file mode 100644 index 000000000..8a981a1cb --- /dev/null +++ b/docs/dev/FLOAT-FUSING.md @@ -0,0 +1,77 @@ +# Fusing Floating Point Operations + +## Why is it needed? + +The current compiler stack only supports integers with 7 bits or less. But it's not uncommon to have numpy code using floating point numbers. + +We added fusing floating point operations to make hnumpy somewhat user friendly to allow in-line quantization in the numpy code e.g.: + +```python +import numpy + +def quantized_sin(x): + # from a 7 bit unsigned integer x, compute z in the [0; 2 * pi] range + z = 2 * numpy.pi * x * (1 / 127) + # quantize over 6 bits and offset to be >= 0, round and convert to integers in range [0; 63] + quantized_sin = numpy.rint(31 * numpy.sin(z) + 31).astype(numpy.int32) + # output quantized_sin and a further offset result + return quantized_sin, quantized_sin + 32 +``` + +This function `quantized_sin` is not strictly supported as is by the compiler as there are floating point intermediate values. However, when looking at the function globally we can see we have a single integer input and a single integer output. As we know the input range we can compute a table to represent the whole computation for each input value, which can later be lowered to a PBS in the FHE world. + +Any computation where there is a single variable integer input and a single integer output can be replaced by an equivalent table look-up. + +The `quantized_sin` graph of operations: + +![](../_static/float_fusing_example/before.png) + +The float subgraph that was detected: + +![](../_static/float_fusing_example/subgraph.png) + +The simplified graph of operations with the float subgraph condensed in an `ArbitraryFunction` node: + +![](../_static/float_fusing_example/after.png) + +## How is it done in HDK? + +The first step consists in detecting where we go from floating point computation back to integers. This allows to identify the potential terminal node of the float subgraph we are going to fuse. + +From the terminal node, we go back up through the nodes until we find nodes that go from integers to floats. If we can guarantee the identified float subgraph has a single variable integer input then we can replace it by an equivalent ArbitraryFunction node. + +An example of a non fusable computation with that technique is: + +```python +import numpy + +def non_fusable(x, y): + x_1 = x + 1.5 # x_1 is now float + y_1 = y + 3.4 # y_1 is now float + add = x_1 + y_1 + add_int = add.astype(numpy.int32) + return add_int +``` + +From `add_int` you will find two `Add` nodes going from int to float (`x_1` and `y_1`) which we cannot represent with a single input table look-up. + +## Possible improvements + +This technique is not perfect because you could try to go further back in the graph to find a single variable input. + +Firstly, it does not cover optimizing the graph, so you can end up with multiple operations, like additions with constants, or two look-up tables in a row, that can trivially be fused but that are not fused, as the optimization work is left to the compiler backend with MLIR and LLVM tooling. This first limitation does not impact the kind of programs that are compilable. + +Secondly, the current approach fails to handle some programs that in practice could be compiled. The following example could be covered by pushing the search to find a single integer input: + +```python +def theoretically_fusable(x): + x_1 = x + 1.5 + x_2 = x + 3.4 + add = x_1 + x_2 + add_int = add.astype(numpy.int32) + return add_int +``` + +Here the whole function is a single int giving a single int (i.e. representable by a table look-up), but the current implementation of fusing is going to find `x_1` and `x_2` as the starting nodes of the float subgraph and say it cannot fuse it. Room for improvement. It is probably a graph coloring problem where we would have to list for all graph's inputs which nodes depend on them. + +At some point having a proper optimization system with patterns and rules to rewrite them could become more interesting than having this ad-hoc system. From 74f0c9600ec854c3e8b82ea3fb0d6b7d67ca512c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 30 Aug 2021 16:41:36 +0200 Subject: [PATCH 0149/1104] fix: update LD_PRELOAD in docker image and workflows --- .github/workflows/continuous-integration.yaml | 7 +++++-- docker/Dockerfile.hdk-dev | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 07e1d1833..03580a534 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -59,12 +59,15 @@ jobs: id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} env: - # TODO: remove this when concrete is statically linked with compiler - LD_PRELOAD: /concrete/target/release/libconcrete_ffi.so + # TODO: remove this when JIT doesn't need this + LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make pytest - name: Notebooks if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + env: + # TODO: remove this when JIT doesn't need this + LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make strip_nb make notebook_timeout diff --git a/docker/Dockerfile.hdk-dev b/docker/Dockerfile.hdk-dev index 70d88956e..7ac427ca5 100644 --- a/docker/Dockerfile.hdk-dev +++ b/docker/Dockerfile.hdk-dev @@ -6,7 +6,7 @@ RUN echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ echo " source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ echo " cd /hdk/ && make setup_env" >> /root/.bashrc && \ echo "fi" >> /root/.bashrc && \ - echo "export LD_PRELOAD=/concrete/target/release/libconcrete_ffi.so" >> /root/.bashrc + echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc WORKDIR /hdk From dbda93639b86eabaf9c83bc112121ca80ab4e9df Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 30 Aug 2021 16:16:35 +0200 Subject: [PATCH 0150/1104] dev(ir): add get_table function to ArbitraryFunction node --- hdk/common/representation/intermediate.py | 25 +++++++++++++++++++++++ tests/common/extensions/test_table.py | 2 ++ 2 files changed, 27 insertions(+) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index bbe6530c5..d9114c4a5 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -9,6 +9,7 @@ from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, mix_scalar_values_determine_holding_dtype, ) +from ..data_types.integers import Integer from ..values import BaseValue, ClearValue, EncryptedValue, TensorValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" @@ -228,6 +229,30 @@ class ArbitraryFunction(IntermediateNode): def label(self) -> str: return self.op_name + def get_table(self) -> List[int]: + """Function to get the table for the current input value of this ArbitraryFunction. + + Returns: + List[int]: The table. + """ + # Check the input is an unsigned integer to be able to build a table + assert isinstance( + self.inputs[0].data_type, Integer + ), "get_table only works for an unsigned Integer input" + assert not self.inputs[ + 0 + ].data_type.is_signed, "get_table only works for an unsigned Integer input" + + min_input_range = self.inputs[0].data_type.min_value() + max_input_range = self.inputs[0].data_type.max_value() + 1 + + table = [ + int(self.evaluate({0: input_value})) + for input_value in range(min_input_range, max_input_range) + ] + + return table + def default_dot_evaluation_function(lhs: Any, rhs: Any) -> Any: """Default python dot implementation for 1D iterable arrays. diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index f936367d1..7de22abe8 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -45,6 +45,8 @@ def test_lookup_table_encrypted_lookup(test_helpers): x = EncryptedValue(Integer(2, is_signed=False)) op_graph = tracing.trace_numpy_function(f, {"x": x}) + assert op_graph.output_nodes[0].get_table() == [3, 6, 0, 2] + ref_graph = nx.MultiDiGraph() # Here is the ASCII drawing of the expected graph: # (x) - (TLU) From 784158741efd484fac216c145c99e56a77217a33 Mon Sep 17 00:00:00 2001 From: youben11 Date: Tue, 17 Aug 2021 10:30:10 +0100 Subject: [PATCH 0151/1104] feat(mlir): TLU conversion --- hdk/common/mlir/converters.py | 32 +++++++++++++++-- tests/common/mlir/test_converters.py | 46 ++++++++++++++++++++++-- tests/common/mlir/test_mlir_converter.py | 14 ++++++++ tests/hnumpy/test_compile.py | 7 ++++ 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py index 520b2a0d4..df615689f 100644 --- a/hdk/common/mlir/converters.py +++ b/hdk/common/mlir/converters.py @@ -6,11 +6,12 @@ Converter functions all have the same signature `converter(node, preds, ir_to_ml - `ir_to_mlir_node`: Dict mapping intermediate nodes to MLIR nodes or values - `ctx`: MLIR context """ -# pylint: disable=no-name-in-module,no-member from typing import cast +# pylint: disable=no-name-in-module,no-member +import numpy as np from mlir.dialects import std as std_dialect -from mlir.ir import IntegerAttr, IntegerType +from mlir.ir import DenseElementsAttr, IntegerAttr, IntegerType, RankedTensorType from zamalang.dialects import hlfhe from ...common.data_types.integers import Integer @@ -129,11 +130,38 @@ def constant(node, _, __, ctx): return std_dialect.ConstantOp(int_type, IntegerAttr.get(int_type, node.constant_data)).result +def apply_lut(node, preds, ir_to_mlir_node, ctx): + """Converted function for the arbitrary function intermediate node.""" + assert len(node.inputs) == 1, "LUT should have a single input" + assert len(node.outputs) == 1, "LUT should have a single output" + if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): + raise TypeError("Only support LUT with encrypted unsigned integers inputs") + if not value_is_encrypted_scalar_unsigned_integer(node.outputs[0]): + raise TypeError("Only support LUT with encrypted unsigned integers outputs") + + x_node = preds[0] + x = ir_to_mlir_node[x_node] + table = node.get_table() + out_dtype = cast(Integer, node.outputs[0].data_type) + # Create table + dense_elem = DenseElementsAttr.get(np.array(table, dtype=np.uint64), context=ctx) + tensor_lut = std_dialect.ConstantOp( + RankedTensorType.get([len(table)], IntegerType.get_signless(64, context=ctx)), + dense_elem, + ).result + return hlfhe.ApplyLookupTableEintOp( + hlfhe.EncryptedIntegerType.get(ctx, out_dtype.bit_width), + x, + tensor_lut, + ).result + + V0_OPSET_CONVERSION_FUNCTIONS = { ir.Add: add, ir.Sub: sub, ir.Mul: mul, ir.Constant: constant, + ir.ArbitraryFunction: apply_lut, } # pylint: enable=no-name-in-module,no-member diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index 6c2ef33ca..c84c34f53 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -3,8 +3,8 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.mlir.converters import add, constant, mul, sub -from hdk.common.values import ClearValue +from hdk.common.mlir.converters import add, apply_lut, constant, mul, sub +from hdk.common.values import ClearValue, EncryptedValue class MockNode: @@ -38,3 +38,45 @@ def test_fail_signed_integer_const(): """Test failing constant converter with non-integer""" with pytest.raises(TypeError, match=r"Don't support signed constant integer"): constant(MockNode(outputs=[ClearValue(Integer(8, True))]), None, None, None) + + +@pytest.mark.parametrize( + "input_node", + [ + ClearValue(Integer(8, True)), + ClearValue(Integer(8, False)), + EncryptedValue(Integer(8, True)), + ], +) +def test_fail_tlu_input(input_node): + """Test failing LUT converter with invalid input""" + with pytest.raises( + TypeError, match=r"Only support LUT with encrypted unsigned integers inputs" + ): + apply_lut( + MockNode(inputs=[input_node], outputs=[EncryptedValue(Integer(8, False))]), + [None], + None, + None, + ) + + +@pytest.mark.parametrize( + "input_node", + [ + ClearValue(Integer(8, True)), + ClearValue(Integer(8, False)), + EncryptedValue(Integer(8, True)), + ], +) +def test_fail_tlu_output(input_node): + """Test failing LUT converter with invalid output""" + with pytest.raises( + TypeError, match=r"Only support LUT with encrypted unsigned integers outputs" + ): + apply_lut( + MockNode(inputs=[EncryptedValue(Integer(8, False))], outputs=[input_node]), + [None], + None, + None, + ) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 5a370d016..b4fadaa42 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -8,6 +8,7 @@ from zamalang import compiler from zamalang.dialects import hlfhe from hdk.common.data_types.integers import Integer +from hdk.common.extensions.table import LookupTable from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from hdk.common.values import ClearValue, EncryptedValue from hdk.hnumpy.compile import compile_numpy_function_into_op_graph @@ -58,6 +59,12 @@ def ret_multiple_different_order(x, y, z): return y, z, x +def lut(x): + """Test lookup table""" + table = LookupTable([3, 6, 0, 2, 1, 4, 5, 7]) + return table[x] + + def datagen(*args): """Generate data from ranges""" for prod in itertools.product(*args): @@ -163,6 +170,13 @@ def datagen(*args): }, (range(1, 5), range(1, 5), range(1, 5)), ), + ( + lut, + { + "x": EncryptedValue(Integer(64, is_signed=False)), + }, + (range(0, 8),), + ), ], ) def test_mlir_converter(func, args_dict, args_ranges): diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 5f04714d9..436abadb0 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -23,6 +23,12 @@ def no_fuse_unhandled(x, y): return intermediate.astype(numpy.int32) +def lut(x): + """Test lookup table""" + table = LookupTable(list(range(128))) + return table[x] + + @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ @@ -75,6 +81,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n pytest.param(lambda x: x * 2, ((0, 2),), ["x"]), pytest.param(lambda x: 8 - x, ((0, 2),), ["x"]), pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), + pytest.param(lut, ((0, 127),), ["x"]), ], ) def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_of_arg_names): From 0f2b7f7d2aaabc65481322bf8e93f401265b58f3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 27 Aug 2021 11:50:06 +0200 Subject: [PATCH 0152/1104] build: allow docker base image build triggered by API call - add a polling script to check packages versions regularly and rebuild if the compiler image is more recent --- .github/workflows/docker-env.yaml | 5 ++++ .github/workflows/package-watcher.yaml | 41 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/package-watcher.yaml diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index 696e23f0b..bf6a7b069 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -10,6 +10,11 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: + # Allows external webhook trigger + repository_dispatch: + types: + - rebuild-docker + jobs: build_publish: concurrency: diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml new file mode 100644 index 000000000..5e251436c --- /dev/null +++ b/.github/workflows/package-watcher.yaml @@ -0,0 +1,41 @@ +name: Package Version Checker + +on: + schedule: + # * is a special character in YAML so you have to quote this string + # At minute 0 for each hour from 8:00 to 22:00 inclusive from Monday to Friday inclusive + - cron: '0 8-22 * * 1-5' + +jobs: + check_and_notify_build: + name: Check timestamps and notify build + runs-on: ubuntu-20.04 + env: + BASE_IMG_ENDPOINT_URL: "https://api.github.com/orgs/zama-ai/packages/container/zamalang-compiler" + ENV_IMG_ENDPOINT_URL: "https://api.github.com/orgs/zama-ai/packages/container/hdk-env" + steps: + - name: Compare image timestamps and notify + - run: | + BASE_IMG_TIMESTAMP=$(curl \ + -X GET \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ + "${BASE_IMG_ENDPOINT_URL}" | jq -r '.updated_at') + ENV_IMG_TIMESTAMP=$(curl \ + -X GET \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ + "${ENV_IMG_ENDPOINT_URL}" | jq -r '.updated_at') + BASE_IMG_DATE=$(date -d ${BASE_IMG_TIMESTAMP} +%s) + ENV_IMG_DATE=$(date -d ${ENV_IMG_TIMESTAMP} +%s) + if [[ "${BASE_IMG_DATE}" -ge "${ENV_IMG_DATE}" ]]; then + echo "Env image out of date, sending rebuild request." + curl \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ + https://api.github.com/repos/zama-ai/hdk/dispatches \ + -d '{"event_type":"rebuild-docker"}' + else + echo "Image up to date, nothing to do." + fi From b9fd9b7b9b22c27eaeb5bba547c0a610659df701 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 31 Aug 2021 10:28:25 +0200 Subject: [PATCH 0153/1104] fix: bad workflow format --- .github/workflows/package-watcher.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index 5e251436c..aaeb9b483 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -15,7 +15,7 @@ jobs: ENV_IMG_ENDPOINT_URL: "https://api.github.com/orgs/zama-ai/packages/container/hdk-env" steps: - name: Compare image timestamps and notify - - run: | + run: | BASE_IMG_TIMESTAMP=$(curl \ -X GET \ -H "Accept: application/vnd.github.v3+json" \ From e408b292d3d93540282887de4cca7d7123aca652 Mon Sep 17 00:00:00 2001 From: youben11 Date: Tue, 31 Aug 2021 10:05:12 +0100 Subject: [PATCH 0154/1104] ci: pulling latest image when building docker img --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 56b60c8c1..cfaabfc30 100644 --- a/Makefile +++ b/Makefile @@ -108,11 +108,11 @@ coverage: .PHONY: coverage docker_build: - docker build -t $(DEV_DOCKER_IMG) -f $(DEV_DOCKERFILE) . + docker build --pull -t $(DEV_DOCKER_IMG) -f $(DEV_DOCKERFILE) . .PHONY: docker_build docker_rebuild: - docker build --no-cache -t $(DEV_DOCKER_IMG) -f $(DEV_DOCKERFILE) . + docker build --pull --no-cache -t $(DEV_DOCKER_IMG) -f $(DEV_DOCKERFILE) . .PHONY: docker_rebuild docker_start: From 758a5727dcb6c3b009cd788265b969c93f3a8694 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 31 Aug 2021 11:33:58 +0200 Subject: [PATCH 0155/1104] fix: disable the package watcher for now - I used the wrong date value, will update in a further PR --- .github/workflows/package-watcher.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index aaeb9b483..f448173c2 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -1,10 +1,11 @@ name: Package Version Checker on: - schedule: - # * is a special character in YAML so you have to quote this string - # At minute 0 for each hour from 8:00 to 22:00 inclusive from Monday to Friday inclusive - - cron: '0 8-22 * * 1-5' + # schedule: + # # * is a special character in YAML so you have to quote this string + # # At minute 0 for each hour from 8:00 to 22:00 inclusive from Monday to Friday inclusive + # - cron: '0 8-22 * * 1-5' + workflow_dispatch: jobs: check_and_notify_build: From 802f7943b1337dd26a2f039d383165c45b38e03a Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 30 Aug 2021 16:27:30 +0200 Subject: [PATCH 0156/1104] feat: adding cos in the managed operators refs #126 closes #235 --- hdk/hnumpy/tracing.py | 45 +++++++++++++++++++----------------- tests/hnumpy/test_tracing.py | 2 ++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index fbd11b881..9ccf24b5b 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -130,22 +130,24 @@ class NPTracer(BaseTracer): ] return common_output_dtypes - def rint(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.rint. + def _unary_operator( + self, unary_operator, unary_operator_string, *input_tracers: "NPTracer", **kwargs + ) -> "NPTracer": + """Function to trace an unary operator. Returns: NPTracer: The output NPTracer containing the traced function """ assert len(input_tracers) == 1 - common_output_dtypes = self._manage_dtypes(numpy.rint, *input_tracers) + common_output_dtypes = self._manage_dtypes(unary_operator, *input_tracers) assert len(common_output_dtypes) == 1 traced_computation = ArbitraryFunction( input_base_value=input_tracers[0].output, - arbitrary_func=numpy.rint, + arbitrary_func=unary_operator, output_dtype=common_output_dtypes[0], op_kwargs=deepcopy(kwargs), - op_name="np.rint", + op_name=unary_operator_string, ) output_tracer = self.__class__( input_tracers, @@ -154,29 +156,29 @@ class NPTracer(BaseTracer): ) return output_tracer + def rint(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.rint. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.rint, "np.rint", *input_tracers, **kwargs) + def sin(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": """Function to trace numpy.sin. Returns: NPTracer: The output NPTracer containing the traced function """ - assert len(input_tracers) == 1 - common_output_dtypes = self._manage_dtypes(numpy.sin, *input_tracers) - assert len(common_output_dtypes) == 1 + return self._unary_operator(numpy.sin, "np.sin", *input_tracers, **kwargs) - traced_computation = ArbitraryFunction( - input_base_value=input_tracers[0].output, - arbitrary_func=numpy.sin, - output_dtype=common_output_dtypes[0], - op_kwargs=deepcopy(kwargs), - op_name="np.sin", - ) - output_tracer = self.__class__( - input_tracers, - traced_computation=traced_computation, - output_index=0, - ) - return output_tracer + def cos(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.cos. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.cos, "np.cos", *input_tracers, **kwargs) def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": """Function to trace numpy.dot. @@ -206,6 +208,7 @@ class NPTracer(BaseTracer): UFUNC_ROUTING: Dict[numpy.ufunc, Callable] = { numpy.rint: rint, numpy.sin: sin, + numpy.cos: cos, } FUNC_ROUTING: Dict[Callable, Callable] = { diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 5e8a76ff6..6fd5de1a8 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -238,6 +238,7 @@ def test_tracing_astype( # pylint: disable=unnecessary-lambda pytest.param(lambda x: numpy.rint(x)), pytest.param(lambda x: numpy.sin(x)), + pytest.param(lambda x: numpy.cos(x)), # The next test case is only for coverage purposes, to trigger the unsupported method # exception handling pytest.param( @@ -348,6 +349,7 @@ def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expec [ pytest.param(numpy.rint, tracing.NPTracer.rint), pytest.param(numpy.sin, tracing.NPTracer.sin), + pytest.param(numpy.cos, tracing.NPTracer.cos), pytest.param(numpy.dot, tracing.NPTracer.dot), # There is a need to test the case where the function fails, I chose numpy.conjugate which # works on complex types, as we don't talk about complex types for now this looks like a From 135805a1b9fd26d6e69401629792e6dfa39c9280 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 27 Aug 2021 15:03:19 +0200 Subject: [PATCH 0157/1104] tools: make matplotlib work inside the dev docker - use special DNS from docker - install tkinter - use proper matplotlib backend - add instructions to install xserver in docs/dev/GETTING-STARTED.md --- Makefile | 6 +++++- docker/Dockerfile.hdk-dev | 3 ++- docker/Dockerfile.hdk-env | 1 + docs/dev/GETTING-STARTED.md | 30 +++++++++++++++++++++++++++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index cfaabfc30..e1e92c38b 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,11 @@ docker_rebuild: docker_start: @# the slash before pwd is for Windows - docker run --rm -it -p 8888:8888 --volume /"$$(pwd)":/hdk $(DEV_DOCKER_IMG) + docker run --rm -it \ + -p 8888:8888 \ + --env DISPLAY=host.docker.internal:0 \ + --volume /"$$(pwd)":/hdk \ + $(DEV_DOCKER_IMG) .PHONY: docker_start docker_build_and_start: docker_build docker_start diff --git a/docker/Dockerfile.hdk-dev b/docker/Dockerfile.hdk-dev index 7ac427ca5..85652c972 100644 --- a/docker/Dockerfile.hdk-dev +++ b/docker/Dockerfile.hdk-dev @@ -6,7 +6,8 @@ RUN echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ echo " source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ echo " cd /hdk/ && make setup_env" >> /root/.bashrc && \ echo "fi" >> /root/.bashrc && \ - echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc + echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc && \ + echo "export MPLBACKEND=TkAgg" >> /root/.bashrc WORKDIR /hdk diff --git a/docker/Dockerfile.hdk-env b/docker/Dockerfile.hdk-env index 26bf5eea4..09ca1995b 100644 --- a/docker/Dockerfile.hdk-env +++ b/docker/Dockerfile.hdk-env @@ -2,6 +2,7 @@ FROM ghcr.io/zama-ai/zamalang-compiler RUN apt-get install --no-install-recommends -y \ python3.8 \ + python3.8-tk \ python3.8-venv \ python-is-python3 \ git \ diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 8c05213f9..63abbe6a8 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -110,9 +110,37 @@ In this section, we will discuss the module structure of hdk briefly. You are en ## Working in Docker +### Setting up docker and X forwarding + Before you start this section, go ahead and install docker. You can follow [this](https://docs.docker.com/engine/install/) official guide for that. -Docker image of `hdk` is based of another docker image provided by the compiler team. The process of building on top of that image is automated, but it requires authorization. So to work in docker, talk with your team lead to gain access to the base docker image. You need to be added to a special group in the organization called `homomorphizer-ghcr`. +#### Linux + +```console +xhost +localhost +``` + +#### Mac OS + +To be able to use X forwarding on Mac OS: first, you need to install xquartz. Secondly, open XQuartz.app application, and open a new terminal within XQuartz.app. Make sure in the application parameters to authorize network connections are set (currently in the Security settings); finally, in the XQuartz.app terminal, type + +```console +xhost +127.0.0.1 +``` + +and now, the X server should be all set in docker (in the regular terminal). + +#### Windows + +Install Xming and use Xlaunch: +- Multiple Windows, Display number: 0 +- Start no client +- **IMPORTANT**: Check `No Access Control` +- You can save this configuration to re-launch easily, then click finish. + +### Logging in and building the image + +Docker image of `hdk` is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. Upon joining to the team, you need to log in using the following command: From 3c6969f7f475335293ecd69ecce6d38b3d3cb960 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 31 Aug 2021 13:43:55 +0200 Subject: [PATCH 0158/1104] tools: updated package watcher with helper script --- .github/workflows/package-watcher.yaml | 43 ++++-------- .../container_timestamp_check.sh | 70 +++++++++++++++++++ 2 files changed, 82 insertions(+), 31 deletions(-) create mode 100755 script/actions_utils/container_timestamp_check.sh diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index f448173c2..1352bf146 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -1,42 +1,23 @@ name: Package Version Checker on: - # schedule: - # # * is a special character in YAML so you have to quote this string - # # At minute 0 for each hour from 8:00 to 22:00 inclusive from Monday to Friday inclusive - # - cron: '0 8-22 * * 1-5' - workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + # At minute 0 for each hour from 8:00 to 22:00 inclusive from Monday to Friday inclusive + - cron: '0 8-22 * * 1-5' jobs: check_and_notify_build: name: Check timestamps and notify build runs-on: ubuntu-20.04 - env: - BASE_IMG_ENDPOINT_URL: "https://api.github.com/orgs/zama-ai/packages/container/zamalang-compiler" - ENV_IMG_ENDPOINT_URL: "https://api.github.com/orgs/zama-ai/packages/container/hdk-env" steps: + - name: Checkout Code + uses: actions/checkout@v2 - name: Compare image timestamps and notify run: | - BASE_IMG_TIMESTAMP=$(curl \ - -X GET \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ - "${BASE_IMG_ENDPOINT_URL}" | jq -r '.updated_at') - ENV_IMG_TIMESTAMP=$(curl \ - -X GET \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ - "${ENV_IMG_ENDPOINT_URL}" | jq -r '.updated_at') - BASE_IMG_DATE=$(date -d ${BASE_IMG_TIMESTAMP} +%s) - ENV_IMG_DATE=$(date -d ${ENV_IMG_TIMESTAMP} +%s) - if [[ "${BASE_IMG_DATE}" -ge "${ENV_IMG_DATE}" ]]; then - echo "Env image out of date, sending rebuild request." - curl \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ - https://api.github.com/repos/zama-ai/hdk/dispatches \ - -d '{"event_type":"rebuild-docker"}' - else - echo "Image up to date, nothing to do." - fi + ./script/actions_utils/container_timestamp_check.sh \ + --base_img_url \ + https://api.github.com/orgs/zama-ai/packages/container/zamalang-compiler/versions \ + --env_img_url \ + https://api.github.com/orgs/zama-ai/packages/container/hdk-env/versions \ + --token ${{ secrets.BOT_TOKEN }} diff --git a/script/actions_utils/container_timestamp_check.sh b/script/actions_utils/container_timestamp_check.sh new file mode 100755 index 000000000..989dfb689 --- /dev/null +++ b/script/actions_utils/container_timestamp_check.sh @@ -0,0 +1,70 @@ +#!/bin/bash -e + +set -e + +BASE_IMG_ENDPOINT_URL= +ENV_IMG_ENDPOINT_URL= +TOKEN= + +while [ -n "$1" ] +do + case "$1" in + "--base_img_url" ) + shift + BASE_IMG_ENDPOINT_URL="$1" + ;; + + "--env_img_url" ) + shift + ENV_IMG_ENDPOINT_URL="$1" + ;; + + "--token" ) + shift + TOKEN="$1" + ;; + + *) + echo "Unknown param : $1" + exit -1 + ;; + esac + shift +done + +BASE_JSON=$(curl \ +-X GET \ +-H "Accept: application/vnd.github.v3+json" \ +-H "Authorization: token ${TOKEN}" \ +"${BASE_IMG_ENDPOINT_URL}") + +BASE_IMG_TIMESTAMP=$(echo "${BASE_JSON}" | jq -r 'sort_by(.updated_at)[-1].updated_at') + +ENV_JSON=$(curl \ +-X GET \ +-H "Accept: application/vnd.github.v3+json" \ +-H "Authorization: token ${TOKEN}" \ +"${ENV_IMG_ENDPOINT_URL}") + +ENV_IMG_TIMESTAMP=$(echo "${ENV_JSON}" | jq -r 'sort_by(.updated_at)[-1].updated_at') + +echo "Base timestamp: ${BASE_IMG_TIMESTAMP}" +echo "Env timestamp: ${ENV_IMG_TIMESTAMP}" + +BASE_IMG_DATE=$(date -d ${BASE_IMG_TIMESTAMP} +%s) +ENV_IMG_DATE=$(date -d ${ENV_IMG_TIMESTAMP} +%s) + +echo "Base epoch: ${BASE_IMG_DATE}" +echo "Env epoch: ${ENV_IMG_DATE}" + +if [[ "${BASE_IMG_DATE}" -ge "${ENV_IMG_DATE}" ]]; then + echo "Env image out of date, sending rebuild request." + curl \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${TOKEN}" \ + https://api.github.com/repos/zama-ai/hdk/dispatches \ + -d '{"event_type":"rebuild-docker"}' +else + echo "Image up to date, nothing to do." +fi From 4c77f0885472388de90e568314a2f922e6ce58b3 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 31 Aug 2021 16:01:44 +0200 Subject: [PATCH 0159/1104] feat: adding tan in the managed operators refs #126 closes #255 --- hdk/hnumpy/tracing.py | 9 +++++++++ tests/hnumpy/test_tracing.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 9ccf24b5b..63f32080a 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -180,6 +180,14 @@ class NPTracer(BaseTracer): """ return self._unary_operator(numpy.cos, "np.cos", *input_tracers, **kwargs) + def tan(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.tan. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.tan, "np.tan", *input_tracers, **kwargs) + def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": """Function to trace numpy.dot. @@ -209,6 +217,7 @@ class NPTracer(BaseTracer): numpy.rint: rint, numpy.sin: sin, numpy.cos: cos, + numpy.tan: tan, } FUNC_ROUTING: Dict[Callable, Callable] = { diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 6fd5de1a8..cc444e4a1 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -239,6 +239,7 @@ def test_tracing_astype( pytest.param(lambda x: numpy.rint(x)), pytest.param(lambda x: numpy.sin(x)), pytest.param(lambda x: numpy.cos(x)), + pytest.param(lambda x: numpy.tan(x)), # The next test case is only for coverage purposes, to trigger the unsupported method # exception handling pytest.param( @@ -350,6 +351,7 @@ def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expec pytest.param(numpy.rint, tracing.NPTracer.rint), pytest.param(numpy.sin, tracing.NPTracer.sin), pytest.param(numpy.cos, tracing.NPTracer.cos), + pytest.param(numpy.tan, tracing.NPTracer.tan), pytest.param(numpy.dot, tracing.NPTracer.dot), # There is a need to test the case where the function fails, I chose numpy.conjugate which # works on complex types, as we don't talk about complex types for now this looks like a From e90df9c0b7cddd5156ce2271905b259b28648a29 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 31 Aug 2021 16:12:11 +0200 Subject: [PATCH 0160/1104] feat: adding arcsin, arccos, arctan in the managed operators refs #126 closes #257 --- hdk/hnumpy/tracing.py | 27 +++++++++++++++++++++++++++ tests/hnumpy/test_tracing.py | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 63f32080a..9fd8b86d1 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -188,6 +188,30 @@ class NPTracer(BaseTracer): """ return self._unary_operator(numpy.tan, "np.tan", *input_tracers, **kwargs) + def arcsin(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.arcsin. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.arcsin, "np.arcsin", *input_tracers, **kwargs) + + def arccos(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.arccos. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.arccos, "np.arccos", *input_tracers, **kwargs) + + def arctan(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.arctan. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.arctan, "np.arctan", *input_tracers, **kwargs) + def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": """Function to trace numpy.dot. @@ -218,6 +242,9 @@ class NPTracer(BaseTracer): numpy.sin: sin, numpy.cos: cos, numpy.tan: tan, + numpy.arcsin: arcsin, + numpy.arccos: arccos, + numpy.arctan: arctan, } FUNC_ROUTING: Dict[Callable, Callable] = { diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index cc444e4a1..cb8b96985 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -240,6 +240,9 @@ def test_tracing_astype( pytest.param(lambda x: numpy.sin(x)), pytest.param(lambda x: numpy.cos(x)), pytest.param(lambda x: numpy.tan(x)), + pytest.param(lambda x: numpy.arcsin(x)), + pytest.param(lambda x: numpy.arccos(x)), + pytest.param(lambda x: numpy.arctan(x)), # The next test case is only for coverage purposes, to trigger the unsupported method # exception handling pytest.param( @@ -352,6 +355,9 @@ def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expec pytest.param(numpy.sin, tracing.NPTracer.sin), pytest.param(numpy.cos, tracing.NPTracer.cos), pytest.param(numpy.tan, tracing.NPTracer.tan), + pytest.param(numpy.arcsin, tracing.NPTracer.arcsin), + pytest.param(numpy.arccos, tracing.NPTracer.arccos), + pytest.param(numpy.arctan, tracing.NPTracer.arctan), pytest.param(numpy.dot, tracing.NPTracer.dot), # There is a need to test the case where the function fails, I chose numpy.conjugate which # works on complex types, as we don't talk about complex types for now this looks like a From 0d84f8c5f524b404271646226ae264d6908e480a Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 31 Aug 2021 16:20:58 +0200 Subject: [PATCH 0161/1104] feat: adding exp functions in the managed operators refs #126 closes #260 --- hdk/hnumpy/tracing.py | 27 +++++++++++++++++++++++++++ tests/hnumpy/test_tracing.py | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index 9fd8b86d1..a5ff23013 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -212,6 +212,30 @@ class NPTracer(BaseTracer): """ return self._unary_operator(numpy.arctan, "np.arctan", *input_tracers, **kwargs) + def exp(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.exp. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.exp, "np.exp", *input_tracers, **kwargs) + + def expm1(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.expm1. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.expm1, "np.expm1", *input_tracers, **kwargs) + + def exp2(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": + """Function to trace numpy.exp2. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + return self._unary_operator(numpy.exp2, "np.exp2", *input_tracers, **kwargs) + def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": """Function to trace numpy.dot. @@ -245,6 +269,9 @@ class NPTracer(BaseTracer): numpy.arcsin: arcsin, numpy.arccos: arccos, numpy.arctan: arctan, + numpy.exp: exp, + numpy.expm1: expm1, + numpy.exp2: exp2, } FUNC_ROUTING: Dict[Callable, Callable] = { diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index cb8b96985..7bc4a4538 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -243,6 +243,9 @@ def test_tracing_astype( pytest.param(lambda x: numpy.arcsin(x)), pytest.param(lambda x: numpy.arccos(x)), pytest.param(lambda x: numpy.arctan(x)), + pytest.param(lambda x: numpy.exp(x)), + pytest.param(lambda x: numpy.expm1(x)), + pytest.param(lambda x: numpy.exp2(x)), # The next test case is only for coverage purposes, to trigger the unsupported method # exception handling pytest.param( @@ -358,6 +361,9 @@ def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expec pytest.param(numpy.arcsin, tracing.NPTracer.arcsin), pytest.param(numpy.arccos, tracing.NPTracer.arccos), pytest.param(numpy.arctan, tracing.NPTracer.arctan), + pytest.param(numpy.exp, tracing.NPTracer.exp), + pytest.param(numpy.expm1, tracing.NPTracer.expm1), + pytest.param(numpy.exp2, tracing.NPTracer.exp2), pytest.param(numpy.dot, tracing.NPTracer.dot), # There is a need to test the case where the function fails, I chose numpy.conjugate which # works on complex types, as we don't talk about complex types for now this looks like a From 02fbbfeaf7e4d239b1c38a0d86bf65b99d261e7c Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 27 Aug 2021 10:36:55 +0300 Subject: [PATCH 0162/1104] test(drawing): crate test for saving the drawing --- tests/common/debugging/test_drawing.py | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/common/debugging/test_drawing.py diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py new file mode 100644 index 000000000..1572996b9 --- /dev/null +++ b/tests/common/debugging/test_drawing.py @@ -0,0 +1,28 @@ +"""Test file for drawing""" + +import tempfile +from pathlib import Path + +from hdk.common.data_types.integers import Integer +from hdk.common.debugging import draw_graph +from hdk.common.values import EncryptedValue +from hdk.hnumpy.compile import compile_numpy_function_into_op_graph + + +def test_draw_graph_with_saving(): + """Tests drawing and saving a graph""" + + def function(x): + return x + 42 + + op_graph = compile_numpy_function_into_op_graph( + function, + {"x": EncryptedValue(Integer(7, True))}, + iter([(-2,), (-1,), (0,), (1,), (2,)]), + ) + + with tempfile.TemporaryDirectory() as tmp: + output_directory = Path(tmp) + output_file = output_directory.joinpath("test.png") + draw_graph(op_graph, save_to=output_file) + assert output_file.exists() From 1fa049d91488be51961eee29f3a60376b30b96bc Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 27 Aug 2021 10:37:28 +0300 Subject: [PATCH 0163/1104] feat: dump compilation artifacts automatically on compilation failures --- .gitignore | 3 + hdk/common/compilation/artifacts.py | 175 +++++++++++-- hdk/common/compilation/configuration.py | 3 + hdk/hnumpy/compile.py | 241 ++++++++++++++---- pylintrc | 2 +- tests/common/compilation/test_artifacts.py | 36 ++- .../common/compilation/test_configuration.py | 6 +- tests/hnumpy/test_compile.py | 4 + 8 files changed, 372 insertions(+), 98 deletions(-) diff --git a/.gitignore b/.gitignore index 242a2ba9b..d187158c0 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,6 @@ dmypy.json # pytest-benchmark results .benchmarks + +# HDK compilation artifacts +.artifacts diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index 32bb585ae..eb6a43bfb 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -1,36 +1,137 @@ """Module for compilation artifacts.""" +import inspect import platform +import shutil import subprocess from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Callable, Dict, Optional, Union import networkx as nx +from PIL import Image from ..debugging import draw_graph, get_printable_graph from ..operator_graph import OPGraph from ..representation import intermediate as ir +from ..values import BaseValue + +DEFAULT_OUTPUT_DIRECTORY: Path = Path(".artifacts") class CompilationArtifacts: """Class that conveys information about compilation process.""" - operation_graph: Optional[OPGraph] - bounds: Optional[Dict[ir.IntermediateNode, Dict[str, Any]]] + output_directory: Path - def __init__(self): - self.operation_graph = None - self.bounds = None + source_code_of_the_function_to_compile: Optional[str] + parameters_of_the_function_to_compile: Dict[str, str] - def export(self, output_directory: Path): - """Exports the artifacts in a textual format. + drawings_of_operation_graphs: Dict[str, Image.Image] + textual_representations_of_operation_graphs: Dict[str, str] + + final_operation_graph: Optional[OPGraph] + bounds_of_the_final_operation_graph: Optional[Dict[ir.IntermediateNode, Dict[str, Any]]] + mlir_of_the_final_operation_graph: Optional[str] + + def __init__(self, output_directory: Path = DEFAULT_OUTPUT_DIRECTORY): + self.output_directory = output_directory + + self.source_code_of_the_function_to_compile = None + self.parameters_of_the_function_to_compile = {} + + self.drawings_of_operation_graphs = {} + self.textual_representations_of_operation_graphs = {} + + self.final_operation_graph = None + self.bounds_of_the_final_operation_graph = None + self.mlir_of_the_final_operation_graph = None + + def add_function_to_compile(self, function: Union[Callable, str]): + """Adds the function to compile to artifacts. Args: - output_directory (Path): the directory to save the artifacts + function (Union[Callable, str]): the function to compile or source code of it Returns: None """ + + self.source_code_of_the_function_to_compile = ( + function if isinstance(function, str) else inspect.getsource(function) + ) + + def add_parameter_of_function_to_compile(self, name: str, value: Union[BaseValue, str]): + """Adds a parameter of the function to compile to the artifacts. + + Args: + name (str): name of the parameter + value (Union[BaseValue, str]): value of the parameter or textual representation of it + + Returns: + None + """ + + self.parameters_of_the_function_to_compile[name] = str(value) + + def add_operation_graph(self, name: str, operation_graph: OPGraph): + """Adds an operation graph to the artifacts. + + Args: + name (str): name of the graph + operation_graph (OPGraph): the operation graph itself + + Returns: + None + """ + + drawing = draw_graph(operation_graph) + textual_representation = get_printable_graph(operation_graph, show_data_types=True)[1:] + + # TODO: remove [1:] above after https://github.com/zama-ai/hdk/issues/222 is fixed + + self.drawings_of_operation_graphs[name] = drawing + self.textual_representations_of_operation_graphs[name] = textual_representation + + self.final_operation_graph = operation_graph + + def add_final_operation_graph_bounds(self, bounds: Dict[ir.IntermediateNode, Dict[str, Any]]): + """Adds the bounds of the final operation graph to the artifacts. + + Args: + bounds (Dict[ir.IntermediateNode, Dict[str, Any]]): the bound dictionary + + Returns: + None + """ + + assert self.final_operation_graph is not None + self.bounds_of_the_final_operation_graph = bounds + + def add_final_operation_graph_mlir(self, mlir: str): + """Adds the mlir of the final operation graph to the artifacts. + + Args: + mlir (str): the mlir code of the final operation graph + + Returns: + None + """ + + assert self.final_operation_graph is not None + self.mlir_of_the_final_operation_graph = mlir + + def export(self): + """Exports the artifacts to a the output directory. + + Returns: + None + """ + + output_directory = self.output_directory + if output_directory.exists(): + shutil.rmtree(output_directory) + output_directory.mkdir() + with open(output_directory.joinpath("environment.txt"), "w") as f: f.write(f"{platform.platform()} {platform.version()}\n") f.write(f"Python {platform.python_version()}\n") @@ -66,23 +167,43 @@ class CompilationArtifacts: f.write(f"{name}=={version}\n") - if self.operation_graph is not None: - with open(output_directory.joinpath("graph.txt"), "w") as f: - f.write(f"{get_printable_graph(self.operation_graph, show_data_types=True)[1:]}\n") + if self.source_code_of_the_function_to_compile is not None: + with open(output_directory.joinpath("function.txt"), "w") as f: + f.write(self.source_code_of_the_function_to_compile) - draw_graph( - self.operation_graph, - show=False, - save_to=output_directory.joinpath("graph.png"), - ) + if len(self.parameters_of_the_function_to_compile) > 0: + with open(output_directory.joinpath("parameters.txt"), "w") as f: + for name, parameter in self.parameters_of_the_function_to_compile.items(): + f.write(f"{name} :: {parameter}\n") - if self.bounds is not None: - with open(output_directory.joinpath("bounds.txt"), "w") as f: - # TODO: - # if nx.topological_sort is not deterministic between calls, - # the lines below will not work properly - # thus, we may want to change this in the future - for index, node in enumerate(nx.topological_sort(self.operation_graph.graph)): - bounds = self.bounds.get(node) - assert bounds is not None - f.write(f"%{index} :: [{bounds.get('min')}, {bounds.get('max')}]\n") + drawings = self.drawings_of_operation_graphs.items() + for index, (name, drawing) in enumerate(drawings): + identifier = CompilationArtifacts._identifier(index, name) + drawing.save(output_directory.joinpath(f"{identifier}.png")) + + textual_representations = self.textual_representations_of_operation_graphs.items() + for index, (name, representation) in enumerate(textual_representations): + identifier = CompilationArtifacts._identifier(index, name) + with open(output_directory.joinpath(f"{identifier}.txt"), "w") as f: + f.write(f"{representation}\n") + + if self.bounds_of_the_final_operation_graph is not None: + assert self.final_operation_graph is not None + with open(output_directory.joinpath("bounds.txt"), "w") as f: + # TODO: + # if nx.topological_sort is not deterministic between calls, + # the lines below will not work properly + # thus, we may want to change this in the future + for index, node in enumerate(nx.topological_sort(self.final_operation_graph.graph)): + bounds = self.bounds_of_the_final_operation_graph.get(node) + assert bounds is not None + f.write(f"%{index} :: [{bounds.get('min')}, {bounds.get('max')}]\n") + + if self.mlir_of_the_final_operation_graph is not None: + assert self.final_operation_graph is not None + with open(output_directory.joinpath("mlir.txt"), "w") as f: + f.write(self.mlir_of_the_final_operation_graph) + + @staticmethod + def _identifier(index, name): + return f"{index + 1}.{name}.graph" diff --git a/hdk/common/compilation/configuration.py b/hdk/common/compilation/configuration.py index 648a6c9a8..c600698e6 100644 --- a/hdk/common/compilation/configuration.py +++ b/hdk/common/compilation/configuration.py @@ -4,10 +4,13 @@ class CompilationConfiguration: """Class that allows the compilation process to be customized.""" + dump_artifacts_on_unexpected_failures: bool enable_topological_optimizations: bool def __init__( self, + dump_artifacts_on_unexpected_failures: bool = True, enable_topological_optimizations: bool = True, ): + self.dump_artifacts_on_unexpected_failures = dump_artifacts_on_unexpected_failures self.enable_topological_optimizations = enable_topological_optimizations diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 98259e0ba..a64bfa495 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -1,5 +1,6 @@ """hnumpy compilation function.""" +import traceback from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple import numpy @@ -47,8 +48,94 @@ def numpy_min_func(lhs: Any, rhs: Any) -> Any: return numpy.minimum(lhs, rhs).min() +def _compile_numpy_function_into_op_graph_internal( + function_to_compile: Callable, + function_parameters: Dict[str, BaseValue], + dataset: Iterator[Tuple[Any, ...]], + compilation_configuration: CompilationConfiguration, + compilation_artifacts: CompilationArtifacts, +) -> OPGraph: + """Compile a function into an OPGraph. + + Args: + function_to_compile (Callable): The function to compile + function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the + function is e.g. an EncryptedValue holding a 7bits unsigned Integer + dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + needs to be an iterator on tuples which are of the same length than the number of + parameters in the function, and in the same order than these same parameters + compilation_artifacts (CompilationArtifacts): Artifacts object to fill + during compilation + compilation_configuration (CompilationConfiguration): Configuration object to use + during compilation + + Returns: + OPGraph: compiled function into a graph + """ + + # Add the function to compile as an artifact + compilation_artifacts.add_function_to_compile(function_to_compile) + + # Add the parameters of function to compile as artifacts + for name, value in function_parameters.items(): + compilation_artifacts.add_parameter_of_function_to_compile(name, str(value)) + + # Trace the function + op_graph = trace_numpy_function(function_to_compile, function_parameters) + + # Add the initial graph as an artifact + compilation_artifacts.add_operation_graph("initial", op_graph) + + # Apply topological optimizations if they are enabled + if compilation_configuration.enable_topological_optimizations: + # Fuse float operations to have int to int ArbitraryFunction + if not check_op_graph_is_integer_program(op_graph): + fuse_float_operations(op_graph) + + # Add the fused floats graph as an artifact + compilation_artifacts.add_operation_graph("fused-float-operations", op_graph) + + # TODO: To be removed once we support more than integers + offending_non_integer_nodes: List[ir.IntermediateNode] = [] + op_grap_is_int_prog = check_op_graph_is_integer_program(op_graph, offending_non_integer_nodes) + if not op_grap_is_int_prog: + raise ValueError( + f"{function_to_compile.__name__} cannot be compiled as it has nodes with either float" + f" inputs or outputs.\nOffending nodes : " + f"{', '.join(str(node) for node in offending_non_integer_nodes)}" + ) + + # Find bounds with the dataset + node_bounds = eval_op_graph_bounds_on_dataset( + op_graph, + dataset, + min_func=numpy_min_func, + max_func=numpy_max_func, + ) + + # Add the bounds as an artifact + compilation_artifacts.add_final_operation_graph_bounds(node_bounds) + + # Update the graph accordingly: after that, we have the compilable graph + op_graph.update_values_with_bounds( + node_bounds, get_base_data_type_for_numpy_or_python_constant_data + ) + + # Add the initial graph as an artifact + compilation_artifacts.add_operation_graph("final", op_graph) + + # Make sure the graph can be lowered to MLIR + if not is_graph_values_compatible_with_mlir(op_graph): + raise TypeError("signed integers aren't supported for MLIR lowering") + + # Update bit_width for MLIR + update_bit_width_for_mlir(op_graph) + + return op_graph + + def compile_numpy_function_into_op_graph( - function_to_trace: Callable, + function_to_compile: Callable, function_parameters: Dict[str, BaseValue], dataset: Iterator[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, @@ -57,7 +144,7 @@ def compile_numpy_function_into_op_graph( """Compile a function into an OPGraph. Args: - function_to_trace (Callable): The function you want to trace + function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedValue holding a 7bits unsigned Integer dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It @@ -79,55 +166,91 @@ def compile_numpy_function_into_op_graph( else compilation_configuration ) - # Trace - op_graph = trace_numpy_function(function_to_trace, function_parameters) + # Create temporary artifacts if custom artifacts is not specified (in case of exceptions) + if compilation_artifacts is None: + compilation_artifacts = CompilationArtifacts() - # Apply topological optimizations if they are enabled - if compilation_configuration.enable_topological_optimizations: - # Fuse float operations to have int to int ArbitraryFunction - if not check_op_graph_is_integer_program(op_graph): - fuse_float_operations(op_graph) - - # TODO: To be removed once we support more than integers - offending_non_integer_nodes: List[ir.IntermediateNode] = [] - op_grap_is_int_prog = check_op_graph_is_integer_program(op_graph, offending_non_integer_nodes) - if not op_grap_is_int_prog: - raise ValueError( - f"{function_to_trace.__name__} cannot be compiled as it has nodes with either float " - f"inputs or outputs.\nOffending nodes : " - f"{', '.join(str(node) for node in offending_non_integer_nodes)}" + # Try to compile the function and save partial artifacts on failure + try: + return _compile_numpy_function_into_op_graph_internal( + function_to_compile, + function_parameters, + dataset, + compilation_configuration, + compilation_artifacts, ) + except Exception: # pragma: no cover + # This branch is reserved for unexpected issues and hence it shouldn't be tested. + # If it could be tested, we would have fixed the underlying issue. - # Find bounds with the dataset - node_bounds = eval_op_graph_bounds_on_dataset( - op_graph, + # We need to export all the information we have about the compilation + # If the user wants them to be exported + + if compilation_configuration.dump_artifacts_on_unexpected_failures: + compilation_artifacts.export() + with open(compilation_artifacts.output_directory.joinpath("traceback.txt"), "w") as f: + f.write(traceback.format_exc()) + + raise + + +def _compile_numpy_function_internal( + function_to_compile: Callable, + function_parameters: Dict[str, BaseValue], + dataset: Iterator[Tuple[Any, ...]], + compilation_configuration: CompilationConfiguration, + compilation_artifacts: CompilationArtifacts, + show_mlir: bool, +) -> CompilerEngine: + """Main API of hnumpy, to be able to compile an homomorphic program. + + Args: + function_to_compile (Callable): The function you want to compile + function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the + function is e.g. an EncryptedValue holding a 7bits unsigned Integer + dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + needs to be an iterator on tuples which are of the same length than the number of + parameters in the function, and in the same order than these same parameters + compilation_configuration (CompilationConfiguration): Configuration object to use + during compilation + compilation_artifacts (CompilationArtifacts): Artifacts object to fill + during compilation + show_mlir (bool): if set, the MLIR produced by the converter and which is going + to be sent to the compiler backend is shown on the screen, e.g., for debugging or demo + + Returns: + CompilerEngine: engine to run and debug the compiled graph + """ + + # Compile into an OPGraph + op_graph = _compile_numpy_function_into_op_graph_internal( + function_to_compile, + function_parameters, dataset, - min_func=numpy_min_func, - max_func=numpy_max_func, + compilation_configuration, + compilation_artifacts, ) - # Update the graph accordingly: after that, we have the compilable graph - op_graph.update_values_with_bounds( - node_bounds, get_base_data_type_for_numpy_or_python_constant_data - ) + # Convert graph to an MLIR representation + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + mlir_result = converter.convert(op_graph) - # Make sure the graph can be lowered to MLIR - if not is_graph_values_compatible_with_mlir(op_graph): - raise TypeError("signed integers aren't supported for MLIR lowering") + # Show MLIR representation if requested + if show_mlir: + print(f"MLIR which is going to be compiled: \n{mlir_result}") - # Update bit_width for MLIR - update_bit_width_for_mlir(op_graph) + # Add MLIR representation as an artifact + compilation_artifacts.add_final_operation_graph_mlir(mlir_result) - # Fill compilation artifacts - if compilation_artifacts is not None: - compilation_artifacts.operation_graph = op_graph - compilation_artifacts.bounds = node_bounds + # Compile the MLIR representation + engine = CompilerEngine() + engine.compile_fhe(mlir_result) - return op_graph + return engine def compile_numpy_function( - function_to_trace: Callable, + function_to_compile: Callable, function_parameters: Dict[str, BaseValue], dataset: Iterator[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, @@ -137,7 +260,7 @@ def compile_numpy_function( """Main API of hnumpy, to be able to compile an homomorphic program. Args: - function_to_trace (Callable): The function you want to trace + function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedValue holding a 7bits unsigned Integer dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It @@ -161,24 +284,30 @@ def compile_numpy_function( else compilation_configuration ) - # Compile into an OPGraph - op_graph = compile_numpy_function_into_op_graph( - function_to_trace, - function_parameters, - dataset, - compilation_configuration, - compilation_artifacts, - ) + # Create temporary artifacts if custom artifacts is not specified (in case of exceptions) + if compilation_artifacts is None: + compilation_artifacts = CompilationArtifacts() - # Convert graph to an MLIR representation - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) - mlir_result = converter.convert(op_graph) + # Try to compile the function and save partial artifacts on failure + try: + return _compile_numpy_function_internal( + function_to_compile, + function_parameters, + dataset, + compilation_configuration, + compilation_artifacts, + show_mlir, + ) + except Exception: # pragma: no cover + # This branch is reserved for unexpected issues and hence it shouldn't be tested. + # If it could be tested, we would have fixed the underlying issue. - if show_mlir: - print(f"MLIR which is going to be compiled: \n{mlir_result}") + # We need to export all the information we have about the compilation + # If the user wants them to be exported - # Compile the MLIR representation - engine = CompilerEngine() - engine.compile_fhe(mlir_result) + if compilation_configuration.dump_artifacts_on_unexpected_failures: + compilation_artifacts.export() + with open(compilation_artifacts.output_directory.joinpath("traceback.txt"), "w") as f: + f.write(traceback.format_exc()) - return engine + raise diff --git a/pylintrc b/pylintrc index d835e1277..3160c4a70 100644 --- a/pylintrc +++ b/pylintrc @@ -548,7 +548,7 @@ valid-metaclass-classmethod-first-arg=cls max-args=10 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=10 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index d3c326f35..b24827e55 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -4,9 +4,9 @@ import tempfile from pathlib import Path from hdk.common.compilation import CompilationArtifacts -from hdk.common.data_types.integers import Integer +from hdk.common.data_types.integers import UnsignedInteger from hdk.common.values import EncryptedValue -from hdk.hnumpy.compile import compile_numpy_function_into_op_graph +from hdk.hnumpy.compile import compile_numpy_function def test_artifacts_export(): @@ -15,23 +15,33 @@ def test_artifacts_export(): def function(x): return x + 42 - artifacts = CompilationArtifacts() - compile_numpy_function_into_op_graph( - function, - {"x": EncryptedValue(Integer(7, True))}, - iter([(-2,), (-1,), (0,), (1,), (2,)]), - compilation_artifacts=artifacts, - ) - with tempfile.TemporaryDirectory() as tmp: output_directory = Path(tmp) - artifacts.export(output_directory) + artifacts = CompilationArtifacts(output_directory) + + compile_numpy_function( + function, + {"x": EncryptedValue(UnsignedInteger(7))}, + iter([(0,), (1,), (2,)]), + compilation_artifacts=artifacts, + ) + + artifacts.export() assert output_directory.joinpath("environment.txt").exists() assert output_directory.joinpath("requirements.txt").exists() - assert output_directory.joinpath("graph.txt").exists() - assert output_directory.joinpath("graph.png").exists() + + assert output_directory.joinpath("function.txt").exists() + assert output_directory.joinpath("parameters.txt").exists() + + assert output_directory.joinpath("1.initial.graph.txt").exists() + assert output_directory.joinpath("1.initial.graph.png").exists() + + assert output_directory.joinpath("2.final.graph.txt").exists() + assert output_directory.joinpath("2.final.graph.png").exists() + assert output_directory.joinpath("bounds.txt").exists() + assert output_directory.joinpath("mlir.txt").exists() # format of those files might change in the future # so it is sufficient to test their existance diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index 890c22f11..bad6c08d9 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -50,6 +50,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused for param in signature(function_to_trace).parameters.keys() }, iter([(1,), (2,), (3,)]), + CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) op_graph_not_optimized = compile_numpy_function_into_op_graph( function_to_trace, @@ -58,7 +59,10 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused for param in signature(function_to_trace).parameters.keys() }, iter([(1,), (2,), (3,)]), - compilation_configuration=CompilationConfiguration(enable_topological_optimizations=False), + CompilationConfiguration( + dump_artifacts_on_unexpected_failures=False, + enable_topological_optimizations=False, + ), ) graph = op_graph.graph diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 436abadb0..06608bfda 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -5,6 +5,7 @@ import random import numpy import pytest +from hdk.common.compilation import CompilationConfiguration from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable @@ -63,6 +64,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) # TODO: For the moment, we don't have really checks, but some printfs. Later, @@ -168,6 +170,7 @@ def test_compile_function_with_direct_tlu_overflow(): function, {"x": EncryptedValue(Integer(3, is_signed=False))}, iter([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)]), + CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) @@ -193,6 +196,7 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) From a7eb2973a646e2b1b5844b29dd4578a272d22729 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 25 Aug 2021 18:09:58 +0300 Subject: [PATCH 0164/1104] chore(Makefile): run `clean_docs` before running `docs` to prevent obsolete warnings --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e1e92c38b..bcfae133c 100644 --- a/Makefile +++ b/Makefile @@ -130,7 +130,7 @@ docker_build_and_start: docker_build docker_start docker_bas: docker_build_and_start .PHONY: docker_bas -docs: +docs: clean_docs @# Generate the auto summary of documentations poetry run sphinx-apidoc -o docs/_apidoc hdk From a9ccba60efec107d689d45f507b098d6508a3b86 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 31 Aug 2021 18:33:12 +0300 Subject: [PATCH 0165/1104] docs: create compilation pipeline in depth document --- .../forty_two_minus_x_plus_y_times_two.png | Bin 0 -> 19960 bytes .../frontend_flow.svg | 0 .../compilation-pipeline/two_x_plus_three.png | Bin 0 -> 17631 bytes docs/dev/ARCHITECTURE.md | 7 - docs/dev/COMPILATION.md | 350 ++++++++++++++++++ docs/index.rst | 3 +- 6 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 docs/_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png rename docs/_static/{ => compilation-pipeline}/frontend_flow.svg (100%) create mode 100644 docs/_static/compilation-pipeline/two_x_plus_three.png delete mode 100644 docs/dev/ARCHITECTURE.md create mode 100644 docs/dev/COMPILATION.md diff --git a/docs/_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png b/docs/_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png new file mode 100644 index 0000000000000000000000000000000000000000..2ec2a408e1bb91ee98fcff661142b77ad7468dbf GIT binary patch literal 19960 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 IW&Gj)0a%L|V*mgE literal 0 HcmV?d00001 diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md deleted file mode 100644 index 41eb9f15e..000000000 --- a/docs/dev/ARCHITECTURE.md +++ /dev/null @@ -1,7 +0,0 @@ -# Architecture of the project - -This is currently very much WIP. - -## Frontend flow - -![Frontend Flow](../_static/frontend_flow.svg) diff --git a/docs/dev/COMPILATION.md b/docs/dev/COMPILATION.md new file mode 100644 index 000000000..945123f83 --- /dev/null +++ b/docs/dev/COMPILATION.md @@ -0,0 +1,350 @@ +# Compilation Pipeline In Depth + +## What is HDK? + +`HDK` is a framework for developing homomorphic applications. +One of its essential functionalities is to transform Python functions to their `MLIR` equivalent. +Unfortunately, not all python functions can be converted due to the limits of current product (we are in the alpha stage), or sometimes due to inherent restrictions of FHE itself. +However, one can already build interesting and impressing use cases, and more will be available in further versions of the framework. + +## How can I use it? + +```python +# Import necessary HDK components +from hdk.common.data_types.integers import UnsignedInteger +from hdk.common.values import EncryptedValue, EncryptedTensor +from hdk.hnumpy.compile import compile_numpy_function + +# Define the function to homomorphize +def f(x, y): + return (2 * x) + y + +# Define the inputs of homomorphized function +x = EncryptedValue(UnsignedInteger(2)) +y = EncryptedValue(UnsignedInteger(1)) + +# Compile the function to its homomorphic equivalent +engine = compile_numpy_function( + f, {"x": x, "y": y}, + iter([(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)]), +) + +# Make homomorphic inference +engine.run([1, 0]) +``` + +## Overview + +The compilation journey begins with tracing to get an easy to understand and manipulate representation of the function. +We call this representation `Operation Graph` which is basically a Directed Acyclic Graph (DAG) containing nodes representing the computations done in the function. +Working with graphs is good because they have been studied extensively over the years and there are a lot of algorithms to manipulate them. +Internally, we use [networkx](https://networkx.org) which is an excellent graph library for Python. + +The next step in the compilation is transforming the operation graph. +There are many transformations we perform, and they will be discussed in their own sections. +In any case, the result of transformations is just another operation graph. + +After transformations are applied, we need to determine the bounds (i.e., the minimum and the maximum values) of each intermediate result. +This is required because FHE currently allows a limited precision for computations. +Bound measurement is our way to know what is the needed precision for the function. +There are several approaches to compute bounds, and they will be discussed in their own sections. + +The final step is to transform the operation graph to equivalent `MLIR` code. +How this is done will be explained in detail in its own chapter. + +Once the MLIR is prepared, the rest of the stack, which you can learn more about [here](http://fixme.com/), takes over and completes the compilation process. + +Here is the visual representation of the pipeline: + +![Frontend Flow](../_static/compilation-pipeline/frontend_flow.svg) + +## Tracing + +Given a Python function `f` such as this one, + +``` +def f(x): + return (2 * x) + 3 +``` + +the goal of tracing is to create the following operation graph without needing any change from the user. + +![](../_static/compilation-pipeline/two_x_plus_three.png) + +(Note that the edge labels are for non-commutative operations. To give an example, a subtraction node represents `(predecessor with edge label 0) - (predecessor with edge label 1)`) + +To do this, we make use of `Tracer`s, which are objects that record the operation performed during their creation. +We create a `Tracer` for each argument of the function and call the function with those tracers. +`Tracer`s make use of operator overloading feature of Python to achieve their goal. + +Here is an example: + +``` +def f(x, y): + return x + 2 * y + +x = Tracer(computation=Input("x")) +y = Tracer(computation=Input("y")) + +resulting_tracer = f(x, y) +``` + +`2 * y` will be performed first, and `*` is overloaded for `Tracer` to return another tracer: +`Tracer(computation=Multiply(Constant(2), self.computation))` which is equal to: +`Tracer(computation=Multiply(Constant(2), Input("y")))` + +`x + (2 * y)` will be performed next, and `+` is overloaded for `Tracer` to return another tracer: +`Tracer(computation=Add(self.computation, (2 * y).computation))` which is equal to: +`Tracer(computation=Add(Input("x"), Multiply(Constant(2), Input("y")))` + +In the end we will have output `Tracer`s that can be used to create the operation graph. +The implementation is a bit more complex than that but the idea is the same. + +Tracing is also responsible for indicating whether the values in the node would be encrypted or not, and the rule for that is if a node has an encrypted predecessor, it is encrypted as well. + +## Topological Transforms + +The goal of topological transforms is to make more functions compilable. + +With the current version of `HDK` floating point inputs and floating point outputs are not supported. +However, if the floating points operations are intermediate operations, they can sometimes be fused into a single table lookup from integer to integer thanks to some specific transforms. + +Let's take a closer look at the transforms we perform today. + +### Fusing Floating Point Operations + +We decided to allocate a whole new chapter to explain float fusing. +You can find it [here](./FLOAT-FUSING.md). + +## Bounds Measurement + +Given an operation graph, goal of the bound measurement step is to assign the minimal data type to each node in the graph. + +Let's say we have an encrypted input that is always between `0` and `10`, we should assign the type `Encrypted` to node of this input as `Encrypted` is the minimal encrypted integer that supports all the values between `0` and `10`. + +If there were negative values in the range, we could have used `intX` instead of `uintX`. + +Bounds measurement is necessary because FHE supports limited precision, and we don't want unexpected behaviour during evaluation of the compiled functions. + +There are several ways to perform bounds measurement. +Let's take a closer look at the options we provide today. + +### Dataset Evaluation + +This is the simplest approach, but it requires a dataset to be provided by the user. + +The dataset is not the dataset in the usual sense of ML as it doesn't require labels. +Rather, it is a set of values which are typical inputs of the function. + +The idea is to evaluate each input in the dataset and record the result of each operation in the operation graph. +Then we compare the evaluation results with the current minimum/maximum values of each node and update the minimum/maximum accordingly. +After the entire dataset is evaluated, we assign a data type to each node using the minimum and the maximum value it contained. + +Here is an example, given this operation graph where `x` is encrypted: + +![](../_static/compilation-pipeline/two_x_plus_three.png) + +and this dataset: + +``` +[2, 3, 1] +``` + +Evaluation Result of `2`: +- `x`: 2 +- `2`: 2 +- `*`: 4 +- `3`: 3 +- `+`: 7 + +New Bounds: +- `x`: [**2**, **2**] +- `2`: [**2**, **2**] +- `*`: [**4**, **4**] +- `3`: [**3**, **3**] +- `+`: [**7**, **7**] + +Evaluation Result of `3`: +- `x`: 3 +- `2`: 2 +- `*`: 6 +- `3`: 3 +- `+`: 9 + +New Bounds: +- `x`: [2, **3**] +- `2`: [2, 2] +- `*`: [4, **6**] +- `3`: [3, 3] +- `+`: [7, **9**] + +Evaluation Result of `1`: +- `x`: 1 +- `2`: 2 +- `*`: 2 +- `3`: 3 +- `+`: 5 + +New Bounds: +- `x`: [**1**, 3] +- `2`: [2, 2] +- `*`: [**2**, 6] +- `3`: [3, 3] +- `+`: [**5**, 9] + +Assigned Data Types: +- `x`: Encrypted\<**uint2**> +- `2`: Clear\<**uint2**> +- `*`: Encrypted\<**uint3**> +- `3`: Clear\<**uint2**> +- `+`: Encrypted\<**uint4**> + +## MLIR Lowering + +TODO: Ayoub + +## Example Walkthrough #1 + +### Function to Homomorphize + +``` +def f(x): + return (2 * x) + 3 +``` + +### Parameters + +``` +x = EncryptedValue(UnsignedInteger(2)) +``` + +#### Corresponding Operation Graph + +![](../_static/compilation-pipeline/two_x_plus_three.png) + +### Topological Transforms + +#### Fusing Floating Point Operations + +This transform isn't applied since the computation doesn't involve any floating point operations. + +### Bounds Measurement Using [2, 3, 1] as Dataset (same settings as above) + +Data Types: +- `x`: Encrypted\<**uint2**> +- `2`: Clear\<**uint2**> +- `*`: Encrypted\<**uint3**> +- `3`: Clear\<**uint2**> +- `+`: Encrypted\<**uint4**> + +### MLIR Lowering + +``` +module { + func @main(%arg0: !HLFHE.eint<4>) -> !HLFHE.eint<4> { + %c3_i5 = constant 3 : i5 + %c2_i5 = constant 2 : i5 + %0 = "HLFHE.mul_eint_int"(%arg0, %c2_i5) : (!HLFHE.eint<4>, i5) -> !HLFHE.eint<4> + %1 = "HLFHE.add_eint_int"(%0, %c3_i5) : (!HLFHE.eint<4>, i5) -> !HLFHE.eint<4> + return %1 : !HLFHE.eint<4> + } +} +``` + + +## Example Walkthrough #2 + +### Function to Homomorphize + +``` +def f(x, y): + return (42 - x) + (y * 2) +``` + +### Parameters + +``` +x = EncryptedValue(UnsignedInteger(3)) +y = EncryptedValue(UnsignedInteger(1)) +``` + +#### Corresponding Operation Graph + +![](../_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png) + +### Topological Transforms + +#### Fusing Floating Point Operations + +This transform isn't applied since the computation doesn't involve any floating point operations. + +### Bounds Measurement Using [(6, 0), (5, 1), (3, 0), (4, 1)] as Dataset + +Evaluation Result of `(6, 0)`: +- `42`: 42 +- `x`: 6 +- `y`: 0 +- `2`: 2 +- `-`: 36 +- `*`: 0 +- `+`: 36 + +Evaluation Result of `(5, 1)`: +- `42`: 42 +- `x`: 5 +- `y`: 1 +- `2`: 2 +- `-`: 37 +- `*`: 2 +- `+`: 39 + +Evaluation Result of `(3, 0)`: +- `42`: 42 +- `x`: 3 +- `y`: 0 +- `2`: 2 +- `-`: 39 +- `*`: 0 +- `+`: 39 + +Evaluation Result of `(4, 1)`: +- `42`: 42 +- `x`: 4 +- `y`: 1 +- `2`: 2 +- `-`: 38 +- `*`: 2 +- `+`: 40 + +Bounds: +- `42`: [42, 42] +- `x`: [3, 6] +- `y`: [0, 1] +- `2`: [2, 2] +- `-`: [36, 39] +- `*`: [0, 2] +- `+`: [36, 40] + +Data Types: +- `42`: Clear\<**uint6**> +- `x`: Encrypted\<**uint3**> +- `y`: Encrypted\<**uint1**> +- `2`: Clear\<**uint2**> +- `-`: Encrypted\<**uint6**> +- `*`: Encrypted\<**uint2**> +- `+`: Encrypted\<**uint6**> + +### MLIR Lowering + +``` +module { + func @main(%arg0: !HLFHE.eint<6>, %arg1: !HLFHE.eint<6>) -> !HLFHE.eint<6> { + %c42_i7 = constant 42 : i7 + %c2_i7 = constant 2 : i7 + %0 = "HLFHE.sub_int_eint"(%c42_i7, %arg0) : (i7, !HLFHE.eint<6>) -> !HLFHE.eint<6> + %1 = "HLFHE.mul_eint_int"(%arg1, %c2_i7) : (!HLFHE.eint<6>, i7) -> !HLFHE.eint<6> + %2 = "HLFHE.add_eint"(%0, %1) : (!HLFHE.eint<6>, !HLFHE.eint<6>) -> !HLFHE.eint<6> + return %2 : !HLFHE.eint<6> + } +} +``` diff --git a/docs/index.rst b/docs/index.rst index 0e4c71a20..dd84fd8f5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,8 +6,9 @@ Homomorphic Development Kit's documentation :maxdepth: 2 :caption: Developer docs - dev/ARCHITECTURE.md dev/GETTING-STARTED.md + dev/COMPILATION.md + dev/FLOAT-FUSING.md .. toctree:: :maxdepth: 5 From 3b3714893b9751752dd994f63c9b9d5c6f99fd05 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 31 Aug 2021 18:35:13 +0300 Subject: [PATCH 0166/1104] fix(getting-started-document): fix the invalid highlighting that cause sphinx warnings --- docs/dev/GETTING-STARTED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 63abbe6a8..976287919 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -24,7 +24,7 @@ On Linux you can install make from your distribution's preferred package manager On Mac OS you can install a more recent version of make via brew: -```consol +```bash # check for gmake which gmake # If you don't have it, it will error out, install gmake From 2badcecd0d206516114a9a2e479b7a760d87ee14 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 31 Aug 2021 16:45:05 +0200 Subject: [PATCH 0167/1104] feat: append \n instead of prepending it in get_printable_graph closes #222 --- hdk/common/compilation/artifacts.py | 4 +- hdk/common/debugging/printing.py | 4 +- tests/hnumpy/test_compile.py | 10 +-- tests/hnumpy/test_debugging.py | 106 ++++++++++++++-------------- 4 files changed, 61 insertions(+), 63 deletions(-) diff --git a/hdk/common/compilation/artifacts.py b/hdk/common/compilation/artifacts.py index eb6a43bfb..28b16d6fa 100644 --- a/hdk/common/compilation/artifacts.py +++ b/hdk/common/compilation/artifacts.py @@ -85,9 +85,7 @@ class CompilationArtifacts: """ drawing = draw_graph(operation_graph) - textual_representation = get_printable_graph(operation_graph, show_data_types=True)[1:] - - # TODO: remove [1:] above after https://github.com/zama-ai/hdk/issues/222 is fixed + textual_representation = get_printable_graph(operation_graph, show_data_types=True) self.drawings_of_operation_graphs[name] = drawing self.textual_representations_of_operation_graphs[name] = textual_representation diff --git a/hdk/common/debugging/printing.py b/hdk/common/debugging/printing.py index 1cea9033e..1c721472a 100644 --- a/hdk/common/debugging/printing.py +++ b/hdk/common/debugging/printing.py @@ -80,12 +80,12 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: if show_data_types: new_line = f"{new_line: <40s} # {output_data_type_to_string(node)}" - returned_str += f"\n{new_line}" + returned_str += f"{new_line}\n" map_table[node] = i i += 1 return_part = ", ".join(["%" + str(map_table[n]) for n in list_of_nodes_which_are_outputs]) - returned_str += f"\nreturn({return_part})" + returned_str += f"return({return_part})\n" return returned_str diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index 06608bfda..e1d98ed30 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -213,10 +213,10 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): (4,), # Remark that, when you do the dot of tensors of 4 values between 0 and 3, # you can get a maximal value of 4*3*3 = 36, ie something on 6 bits - "\n%0 = x # Integer" + "%0 = x # Integer" "\n%1 = y # Integer" "\n%2 = Dot(0, 1) # Integer" - "\nreturn(%2)", + "\nreturn(%2)\n", ), # pylint: enable=unnecessary-lambda ], @@ -243,9 +243,9 @@ def test_compile_function_with_dot(function, params, shape, ref_graph_str): ) str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) assert str_of_the_graph == ref_graph_str, ( - f"\n==================\nGot {str_of_the_graph}" - f"\n==================\nExpected {ref_graph_str}" - f"\n==================\n" + f"\n==================\nGot \n{str_of_the_graph}" + f"==================\nExpected \n{ref_graph_str}" + f"==================\n" ) diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index 5ef0aaed0..e69bd63fb 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -40,26 +40,26 @@ def issue_130_c(x, y): @pytest.mark.parametrize( "lambda_f,ref_graph_str", [ - (lambda x, y: x + y, "\n%0 = x\n%1 = y\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: x - y, "\n%0 = x\n%1 = y\n%2 = Sub(0, 1)\nreturn(%2)"), - (lambda x, y: x + x, "\n%0 = x\n%1 = Add(0, 0)\nreturn(%1)"), + (lambda x, y: x + y, "%0 = x\n%1 = y\n%2 = Add(0, 1)\nreturn(%2)\n"), + (lambda x, y: x - y, "%0 = x\n%1 = y\n%2 = Sub(0, 1)\nreturn(%2)\n"), + (lambda x, y: x + x, "%0 = x\n%1 = Add(0, 0)\nreturn(%1)\n"), ( lambda x, y: x + x - y * y * y + x, - "\n%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" - "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)\nreturn(%6)", + "%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" + "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)\nreturn(%6)\n", ), - (lambda x, y: x + 1, "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: 1 + x, "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: (-1) + x, "\n%0 = x\n%1 = Constant(-1)\n%2 = Add(0, 1)\nreturn(%2)"), - (lambda x, y: 3 * x, "\n%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)"), - (lambda x, y: x * 3, "\n%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)"), - (lambda x, y: x * (-3), "\n%0 = x\n%1 = Constant(-3)\n%2 = Mul(0, 1)\nreturn(%2)"), - (lambda x, y: x - 11, "\n%0 = x\n%1 = Constant(11)\n%2 = Sub(0, 1)\nreturn(%2)"), - (lambda x, y: 11 - x, "\n%0 = Constant(11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), - (lambda x, y: (-11) - x, "\n%0 = Constant(-11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)"), + (lambda x, y: x + 1, "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)\n"), + (lambda x, y: 1 + x, "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)\n"), + (lambda x, y: (-1) + x, "%0 = x\n%1 = Constant(-1)\n%2 = Add(0, 1)\nreturn(%2)\n"), + (lambda x, y: 3 * x, "%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)\n"), + (lambda x, y: x * 3, "%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)\n"), + (lambda x, y: x * (-3), "%0 = x\n%1 = Constant(-3)\n%2 = Mul(0, 1)\nreturn(%2)\n"), + (lambda x, y: x - 11, "%0 = x\n%1 = Constant(11)\n%2 = Sub(0, 1)\nreturn(%2)\n"), + (lambda x, y: 11 - x, "%0 = Constant(11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)\n"), + (lambda x, y: (-11) - x, "%0 = Constant(-11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)\n"), ( lambda x, y: x + 13 - y * (-21) * y + 44, - "\n%0 = Constant(44)" + "%0 = Constant(44)" "\n%1 = x" "\n%2 = Constant(13)" "\n%3 = y" @@ -69,48 +69,48 @@ def issue_130_c(x, y): "\n%7 = Mul(6, 3)" "\n%8 = Sub(5, 7)" "\n%9 = Add(8, 0)" - "\nreturn(%9)", + "\nreturn(%9)\n", ), # Multiple outputs ( lambda x, y: (x + 1, x + y + 2), - "\n%0 = x" + "%0 = x" "\n%1 = Constant(1)" "\n%2 = Constant(2)" "\n%3 = y" "\n%4 = Add(0, 1)" "\n%5 = Add(0, 3)" "\n%6 = Add(5, 2)" - "\nreturn(%4, %6)", + "\nreturn(%4, %6)\n", ), ( lambda x, y: (y, x), - "\n%0 = y\n%1 = x\nreturn(%0, %1)", + "%0 = y\n%1 = x\nreturn(%0, %1)\n", ), ( lambda x, y: (x, x + 1), - "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%0, %2)", + "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%0, %2)\n", ), ( lambda x, y: (x + 1, x + 1), - "\n%0 = x" + "%0 = x" "\n%1 = Constant(1)" "\n%2 = Constant(1)" "\n%3 = Add(0, 1)" "\n%4 = Add(0, 2)" - "\nreturn(%3, %4)", + "\nreturn(%3, %4)\n", ), ( issue_130_a, - "\n%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2, %2)", + "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2, %2)\n", ), ( issue_130_b, - "\n%0 = x\n%1 = Constant(1)\n%2 = Sub(0, 1)\nreturn(%2, %2)", + "%0 = x\n%1 = Constant(1)\n%2 = Sub(0, 1)\nreturn(%2, %2)\n", ), ( issue_130_c, - "\n%0 = Constant(1)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2, %2)", + "%0 = Constant(1)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2, %2)\n", ), ], ) @@ -143,9 +143,9 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): str_of_the_graph = get_printable_graph(graph) assert str_of_the_graph == ref_graph_str, ( - f"\n==================\nGot {str_of_the_graph}" - f"\n==================\nExpected {ref_graph_str}" - f"\n==================\n" + f"\n==================\nGot \n{str_of_the_graph}" + f"==================\nExpected \n{ref_graph_str}" + f"==================\n" ) @@ -155,12 +155,12 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x\n%1 = TLU(0)\nreturn(%1)", + "%0 = x\n%1 = TLU(0)\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x\n%1 = Constant(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)", + "%0 = x\n%1 = Constant(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)\n", ), ], ) @@ -173,9 +173,9 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph str_of_the_graph = get_printable_graph(graph) assert str_of_the_graph == ref_graph_str, ( - f"\n==================\nGot {str_of_the_graph}" - f"\n==================\nExpected {ref_graph_str}" - f"\n==================\n" + f"\n==================\nGot \n{str_of_the_graph}" + f"==================\nExpected \n{ref_graph_str}" + f"==================\n" ) @@ -189,7 +189,7 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph "x": EncryptedTensor(Integer(2, is_signed=False), shape=(3,)), "y": EncryptedTensor(Integer(2, is_signed=False), shape=(3,)), }, - "\n%0 = x\n%1 = y\n%2 = Dot(0, 1)\nreturn(%2)", + "%0 = x\n%1 = y\n%2 = Dot(0, 1)\nreturn(%2)\n", ), # pylint: enable=unnecessary-lambda ], @@ -203,9 +203,9 @@ def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): str_of_the_graph = get_printable_graph(graph) assert str_of_the_graph == ref_graph_str, ( - f"\n==================\nGot {str_of_the_graph}" - f"\n==================\nExpected {ref_graph_str}" - f"\n==================\n" + f"\n==================\nGot \n{str_of_the_graph}" + f"==================\nExpected \n{ref_graph_str}" + f"==================\n" ) @@ -221,10 +221,10 @@ def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): EncryptedValue(Integer(64, is_signed=False)), EncryptedValue(Integer(32, is_signed=True)), ), - "\n%0 = x # Integer" + "%0 = x # Integer" "\n%1 = y # Integer" "\n%2 = Add(0, 1) # Integer" - "\nreturn(%2)", + "\nreturn(%2)\n", ), ( lambda x, y: x * y, @@ -232,10 +232,10 @@ def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): EncryptedValue(Integer(17, is_signed=False)), EncryptedValue(Integer(23, is_signed=False)), ), - "\n%0 = x # Integer" + "%0 = x # Integer" "\n%1 = y # Integer" "\n%2 = Mul(0, 1) # Integer" - "\nreturn(%2)", + "\nreturn(%2)\n", ), ], ) @@ -247,9 +247,9 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): str_of_the_graph = get_printable_graph(graph, show_data_types=True) assert str_of_the_graph == ref_graph_str, ( - f"\n==================\nGot {str_of_the_graph}" - f"\n==================\nExpected {ref_graph_str}" - f"\n==================\n" + f"\n==================\nGot \n{str_of_the_graph}" + f"==================\nExpected \n{ref_graph_str}" + f"==================\n" ) @@ -259,28 +259,28 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x # Integer" + "%0 = x # Integer" "\n%1 = TLU(0) # Integer" - "\nreturn(%1)", + "\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x # Integer" + "%0 = x # Integer" "\n%1 = Constant(4) # Integer" "\n%2 = Add(0, 1) # Integer" "\n%3 = TLU(2) # Integer" - "\nreturn(%3)", + "\nreturn(%3)\n", ), ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[LOOKUP_TABLE_FROM_3B_TO_2B[x + 4]], {"x": EncryptedValue(Integer(2, is_signed=False))}, - "\n%0 = x # Integer" + "%0 = x # Integer" "\n%1 = Constant(4) # Integer" "\n%2 = Add(0, 1) # Integer" "\n%3 = TLU(2) # Integer" "\n%4 = TLU(3) # Integer" - "\nreturn(%4)", + "\nreturn(%4)\n", ), ], ) @@ -293,7 +293,7 @@ def test_hnumpy_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref str_of_the_graph = get_printable_graph(graph, show_data_types=True) assert str_of_the_graph == ref_graph_str, ( - f"\n==================\nGot {str_of_the_graph}" - f"\n==================\nExpected {ref_graph_str}" - f"\n==================\n" + f"\n==================\nGot \n{str_of_the_graph}" + f"==================\nExpected \n{ref_graph_str}" + f"==================\n" ) From 3b339b1e88406993c41a1d11c8d64e1a9c106734 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 31 Aug 2021 16:00:52 +0200 Subject: [PATCH 0168/1104] tools: make sphinx-build warnings errors - add -W and --keep-going for SPHINXOPTS - use $(MAKE) for make invocations - build the docs in the conformance phase as the sphinx build has checks --- .github/workflows/continuous-integration.yaml | 26 +++++++++---------- Makefile | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 03580a534..8ace055a7 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -49,12 +49,23 @@ jobs: python -m pip install --upgrade pip python -m pip install poetry make setup_env - - name: Conformance + - name: Conformance and Docs build id: conformance if: ${{ success() && !cancelled() }} + env: + # TODO: remove this when JIT doesn't need this + # Required to be sure that docs reads all files with MLIR imports properly + LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so # pcc launches an internal target with proper flags + # docs is run here too as it can fail and we catch errors during the build run: | - make pcc + ./script/make_utils/serialize_targets.sh make pcc docs + - name: Archive docs artifacts + if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + uses: actions/upload-artifact@v2 + with: + name: html-docs + path: docs/_build/html - name: PyTest id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} @@ -89,17 +100,6 @@ jobs: with: path: diff-coverage.txt recreate: true - - name: Build docs - id: docs - if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} - run: | - make docs - - name: Archive docs artifacts - if: ${{ steps.docs.outcome == 'success' && !cancelled() }} - uses: actions/upload-artifact@v2 - with: - name: html-docs - path: docs/_build/html - name: Slack Notification if: ${{ always() }} uses: rtCamp/action-slack-notify@v2 diff --git a/Makefile b/Makefile index bcfae133c..fb3333845 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ setup_env: sync_env: poetry install --remove-untracked - make setup_env + $(MAKE) setup_env .PHONY: sync_env python_format: @@ -135,7 +135,7 @@ docs: clean_docs poetry run sphinx-apidoc -o docs/_apidoc hdk @# Docs - cd docs && poetry run make html + cd docs && poetry run $(MAKE) html SPHINXOPTS='-W --keep-going' .PHONY: docs clean_docs: From 914fb19ebb5ef2c796cd9f8d9642a36c1ad2b87c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 1 Sep 2021 09:55:11 +0200 Subject: [PATCH 0169/1104] tools: adapt UTC timezone for package watcher to match Paris Time --- .github/workflows/package-watcher.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index 1352bf146..d72e6c2fd 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -4,7 +4,8 @@ on: schedule: # * is a special character in YAML so you have to quote this string # At minute 0 for each hour from 8:00 to 22:00 inclusive from Monday to Friday inclusive - - cron: '0 8-22 * * 1-5' + # Timezone is UTC, so Paris time is +2 during the summer and +1 during winter + - cron: '0 6-20 * * 1-5' jobs: check_and_notify_build: From ad0a65078f8ed0602f717e6d7e9861f9359a11e9 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 1 Sep 2021 10:37:11 +0200 Subject: [PATCH 0170/1104] tools: remove serialize_targets.sh which is useless - use make --keep-going instead --- .github/workflows/continuous-integration.yaml | 2 +- Makefile | 6 ++---- script/make_utils/serialize_targets.sh | 18 ------------------ 3 files changed, 3 insertions(+), 23 deletions(-) delete mode 100755 script/make_utils/serialize_targets.sh diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 8ace055a7..b2e21364e 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -59,7 +59,7 @@ jobs: # pcc launches an internal target with proper flags # docs is run here too as it can fail and we catch errors during the build run: | - ./script/make_utils/serialize_targets.sh make pcc docs + make --keep-going pcc docs - name: Archive docs artifacts if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} uses: actions/upload-artifact@v2 diff --git a/Makefile b/Makefile index fb3333845..d68796f11 100644 --- a/Makefile +++ b/Makefile @@ -30,8 +30,7 @@ check_strip_nb: .PHONY: strip_nb pylint: - +poetry run env bash script/make_utils/serialize_targets.sh "$(MAKE)" \ - pylint_src pylint_tests pylint_benchmarks + $(MAKE) --keep-going pylint_src pylint_tests pylint_benchmarks .PHONY: pylint pylint_src: @@ -94,8 +93,7 @@ mypy_benchmark: # the parent make execution. We serialize calls to these targets as they may overwrite each others # cache which can cause issues. mypy_ci: - +poetry run env bash script/make_utils/serialize_targets.sh "$(MAKE)" \ - mypy mypy_test mypy_benchmark + $(MAKE) --keep-going mypy mypy_test mypy_benchmark .PHONY: mypy_ci pytest_and_coverage: pytest coverage diff --git a/script/make_utils/serialize_targets.sh b/script/make_utils/serialize_targets.sh deleted file mode 100755 index c018ad80f..000000000 --- a/script/make_utils/serialize_targets.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -set +e - -EXIT_CODE=0 - -# Get the make executable -MAKE="$1" -shift - -for make_target in "$@"; do - "${MAKE}" --no-print-directory "${make_target}" - if [[ "$?" != "0" ]]; then - EXIT_CODE=1 - fi -done - -exit "${EXIT_CODE}" From 95f683f37cc86ab598db396455b0719d54a089e0 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 31 Aug 2021 10:19:38 +0200 Subject: [PATCH 0171/1104] refacto: do not convert to int in get_table from ArbitraryFunc - this assumption only holds for compiler v0 and there are checks in the MLIR converter to verify that the ArbitraryFunction yields integers --- hdk/common/representation/intermediate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index d9114c4a5..4a2ce8be4 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -229,11 +229,11 @@ class ArbitraryFunction(IntermediateNode): def label(self) -> str: return self.op_name - def get_table(self) -> List[int]: + def get_table(self) -> List[Any]: """Function to get the table for the current input value of this ArbitraryFunction. Returns: - List[int]: The table. + List[Any]: The table. """ # Check the input is an unsigned integer to be able to build a table assert isinstance( @@ -247,7 +247,7 @@ class ArbitraryFunction(IntermediateNode): max_input_range = self.inputs[0].data_type.max_value() + 1 table = [ - int(self.evaluate({0: input_value})) + self.evaluate({0: input_value}) for input_value in range(min_input_range, max_input_range) ] From 1e8debfb578b03fb337a9a3c89de2f8cf0a772f5 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 1 Sep 2021 14:28:02 +0300 Subject: [PATCH 0172/1104] refactor: rename ClearValue/EncryptedValue to ClearScalar/EncryptedScalar --- benchmarks/test_compilation_and_evaluation.py | 10 +- docs/dev/COMPILATION.md | 12 +-- examples/QuantizedLinearRegression.ipynb | 4 +- examples/QuantizedLogisticRegression.ipynb | 6 +- hdk/common/data_types/dtypes_helpers.py | 8 +- hdk/common/representation/intermediate.py | 6 +- hdk/common/values/__init__.py | 2 +- hdk/common/values/scalars.py | 4 +- hdk/hnumpy/compile.py | 8 +- hdk/hnumpy/tracing.py | 2 +- .../bounds_measurement/test_dataset_eval.py | 4 +- tests/common/compilation/test_artifacts.py | 4 +- .../common/compilation/test_configuration.py | 6 +- .../common/data_types/test_dtypes_helpers.py | 62 ++++++------- tests/common/debugging/test_drawing.py | 4 +- tests/common/extensions/test_table.py | 6 +- tests/common/mlir/test_converters.py | 22 ++--- tests/common/mlir/test_mlir_converter.py | 60 ++++++------ .../common/optimization/test_float_fusing.py | 4 +- .../representation/test_intermediate.py | 92 ++++++++++--------- tests/common/test_common_helpers.py | 4 +- tests/hnumpy/test_compile.py | 16 ++-- tests/hnumpy/test_debugging.py | 28 +++--- tests/hnumpy/test_tracing.py | 50 +++++----- 24 files changed, 216 insertions(+), 208 deletions(-) diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py index 587c68cc7..e2c3e4c9e 100644 --- a/benchmarks/test_compilation_and_evaluation.py +++ b/benchmarks/test_compilation_and_evaluation.py @@ -5,7 +5,7 @@ import itertools import pytest from hdk.common.data_types.integers import SignedInteger, UnsignedInteger -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.compile import compile_numpy_function_into_op_graph @@ -14,13 +14,13 @@ from hdk.hnumpy.compile import compile_numpy_function_into_op_graph [ pytest.param( lambda x: x + 42, - {"x": EncryptedValue(SignedInteger(4))}, + {"x": EncryptedScalar(SignedInteger(4))}, ((-2, 2),), id="x + 42", ), pytest.param( lambda x, y: x + y, - {"x": EncryptedValue(SignedInteger(4)), "y": EncryptedValue(UnsignedInteger(4))}, + {"x": EncryptedScalar(SignedInteger(4)), "y": EncryptedScalar(UnsignedInteger(4))}, ((-2, 2), (20, 30)), id="x + y", ), @@ -43,7 +43,7 @@ def test_compilation(benchmark, function, parameters, ranges): [ pytest.param( lambda x: x + 420, - {"x": EncryptedValue(SignedInteger(4))}, + {"x": EncryptedScalar(SignedInteger(4))}, ((-2, 2),), [ {0: -2}, @@ -54,7 +54,7 @@ def test_compilation(benchmark, function, parameters, ranges): ), pytest.param( lambda x, y: x + y, - {"x": EncryptedValue(SignedInteger(4)), "y": EncryptedValue(UnsignedInteger(4))}, + {"x": EncryptedScalar(SignedInteger(4)), "y": EncryptedScalar(UnsignedInteger(4))}, ((-2, 2), (20, 30)), [ {0: -2, 1: 25}, diff --git a/docs/dev/COMPILATION.md b/docs/dev/COMPILATION.md index 945123f83..df07b2de3 100644 --- a/docs/dev/COMPILATION.md +++ b/docs/dev/COMPILATION.md @@ -12,7 +12,7 @@ However, one can already build interesting and impressing use cases, and more wi ```python # Import necessary HDK components from hdk.common.data_types.integers import UnsignedInteger -from hdk.common.values import EncryptedValue, EncryptedTensor +from hdk.common.values import EncryptedScalar, EncryptedTensor from hdk.hnumpy.compile import compile_numpy_function # Define the function to homomorphize @@ -20,8 +20,8 @@ def f(x, y): return (2 * x) + y # Define the inputs of homomorphized function -x = EncryptedValue(UnsignedInteger(2)) -y = EncryptedValue(UnsignedInteger(1)) +x = EncryptedScalar(UnsignedInteger(2)) +y = EncryptedScalar(UnsignedInteger(1)) # Compile the function to its homomorphic equivalent engine = compile_numpy_function( @@ -215,7 +215,7 @@ def f(x): ### Parameters ``` -x = EncryptedValue(UnsignedInteger(2)) +x = EncryptedScalar(UnsignedInteger(2)) ``` #### Corresponding Operation Graph @@ -264,8 +264,8 @@ def f(x, y): ### Parameters ``` -x = EncryptedValue(UnsignedInteger(3)) -y = EncryptedValue(UnsignedInteger(1)) +x = EncryptedScalar(UnsignedInteger(3)) +y = EncryptedScalar(UnsignedInteger(1)) ``` #### Corresponding Operation Graph diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index a552f080b..191a0618e 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -622,7 +622,7 @@ "outputs": [], "source": [ "from hdk.common.data_types.integers import Integer\n", - "from hdk.common.values import EncryptedValue\n", + "from hdk.common.values import EncryptedScalar\n", "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", @@ -631,7 +631,7 @@ "\n", "homomorphic_model = compile_numpy_function_into_op_graph(\n", " infer,\n", - " {\"x_0\": EncryptedValue(Integer(input_bits, is_signed=False))},\n", + " {\"x_0\": EncryptedScalar(Integer(input_bits, is_signed=False))},\n", " iter(dataset),\n", ")" ] diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 834cac1d2..9dd173755 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -726,7 +726,7 @@ "outputs": [], "source": [ "from hdk.common.data_types.integers import Integer\n", - "from hdk.common.values import EncryptedValue\n", + "from hdk.common.values import EncryptedScalar\n", "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", @@ -736,8 +736,8 @@ "homomorphic_model = compile_numpy_function_into_op_graph(\n", " infer,\n", " {\n", - " \"x_0\": EncryptedValue(Integer(input_bits, is_signed=False)),\n", - " \"x_1\": EncryptedValue(Integer(input_bits, is_signed=False)),\n", + " \"x_0\": EncryptedScalar(Integer(input_bits, is_signed=False)),\n", + " \"x_1\": EncryptedScalar(Integer(input_bits, is_signed=False)),\n", " },\n", " iter(dataset),\n", ")" diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 41fb98e80..7502c3a29 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -6,10 +6,10 @@ from typing import Callable, Union, cast from ..values import ( BaseValue, + ClearScalar, ClearTensor, - ClearValue, + EncryptedScalar, EncryptedTensor, - EncryptedValue, ScalarValue, TensorValue, ) @@ -169,9 +169,9 @@ def mix_scalar_values_determine_holding_dtype( mixed_value: ScalarValue if value1.is_encrypted or value2.is_encrypted: - mixed_value = EncryptedValue(holding_type) + mixed_value = EncryptedScalar(holding_type) else: - mixed_value = ClearValue(holding_type) + mixed_value = ClearScalar(holding_type) return mixed_value diff --git a/hdk/common/representation/intermediate.py b/hdk/common/representation/intermediate.py index 4a2ce8be4..3bcf86507 100644 --- a/hdk/common/representation/intermediate.py +++ b/hdk/common/representation/intermediate.py @@ -10,7 +10,7 @@ from ..data_types.dtypes_helpers import ( mix_scalar_values_determine_holding_dtype, ) from ..data_types.integers import Integer -from ..values import BaseValue, ClearValue, EncryptedValue, TensorValue +from ..values import BaseValue, ClearScalar, EncryptedScalar, TensorValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" @@ -292,9 +292,9 @@ class Dot(IntermediateNode): ), f"Dot only supports two vectors ({TensorValue.__name__} with ndim == 1)" output_scalar_value = ( - EncryptedValue + EncryptedScalar if (self.inputs[0].is_encrypted or self.inputs[1].is_encrypted) - else ClearValue + else ClearScalar ) self.outputs = [output_scalar_value(output_dtype)] diff --git a/hdk/common/values/__init__.py b/hdk/common/values/__init__.py index 944df24c4..b8c18b684 100644 --- a/hdk/common/values/__init__.py +++ b/hdk/common/values/__init__.py @@ -1,5 +1,5 @@ """Module for value structures.""" from .base import BaseValue -from .scalars import ClearValue, EncryptedValue, ScalarValue +from .scalars import ClearScalar, EncryptedScalar, ScalarValue from .tensors import ClearTensor, EncryptedTensor, TensorValue diff --git a/hdk/common/values/scalars.py b/hdk/common/values/scalars.py index 8ae8efcbd..c408b1637 100644 --- a/hdk/common/values/scalars.py +++ b/hdk/common/values/scalars.py @@ -35,5 +35,5 @@ def make_encrypted_scalar(data_type: BaseDataType) -> ScalarValue: return ScalarValue(data_type=data_type, is_encrypted=True) -ClearValue = make_clear_scalar -EncryptedValue = make_encrypted_scalar +ClearScalar = make_clear_scalar +EncryptedScalar = make_encrypted_scalar diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index a64bfa495..51208b96f 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -60,7 +60,7 @@ def _compile_numpy_function_into_op_graph_internal( Args: function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the - function is e.g. an EncryptedValue holding a 7bits unsigned Integer + function is e.g. an EncryptedScalar holding a 7bits unsigned Integer dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters @@ -146,7 +146,7 @@ def compile_numpy_function_into_op_graph( Args: function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the - function is e.g. an EncryptedValue holding a 7bits unsigned Integer + function is e.g. an EncryptedScalar holding a 7bits unsigned Integer dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters @@ -207,7 +207,7 @@ def _compile_numpy_function_internal( Args: function_to_compile (Callable): The function you want to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the - function is e.g. an EncryptedValue holding a 7bits unsigned Integer + function is e.g. an EncryptedScalar holding a 7bits unsigned Integer dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters @@ -262,7 +262,7 @@ def compile_numpy_function( Args: function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the - function is e.g. an EncryptedValue holding a 7bits unsigned Integer + function is e.g. an EncryptedScalar holding a 7bits unsigned Integer dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index a5ff23013..bb31aa746 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -287,7 +287,7 @@ def trace_numpy_function( Args: function_to_trace (Callable): The function you want to trace function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the - function is e.g. an EncryptedValue holding a 7bits unsigned Integer + function is e.g. an EncryptedScalar holding a 7bits unsigned Integer Returns: OPGraph: The graph containing the ir nodes representing the computation done in the input diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index 210b138e5..d87b510ae 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -7,7 +7,7 @@ import pytest from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.tracing import trace_numpy_function @@ -271,7 +271,7 @@ def test_eval_op_graph_bounds_on_dataset_multiple_output( """Test function for eval_op_graph_bounds_on_dataset""" op_graph = trace_numpy_function( - function, {"x": EncryptedValue(Integer(64, True)), "y": EncryptedValue(Integer(64, True))} + function, {"x": EncryptedScalar(Integer(64, True)), "y": EncryptedScalar(Integer(64, True))} ) def data_gen(range_x, range_y): diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index b24827e55..0c992f329 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -5,7 +5,7 @@ from pathlib import Path from hdk.common.compilation import CompilationArtifacts from hdk.common.data_types.integers import UnsignedInteger -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.compile import compile_numpy_function @@ -21,7 +21,7 @@ def test_artifacts_export(): compile_numpy_function( function, - {"x": EncryptedValue(UnsignedInteger(7))}, + {"x": EncryptedScalar(UnsignedInteger(7))}, iter([(0,), (1,), (2,)]), compilation_artifacts=artifacts, ) diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index bad6c08d9..f6668a816 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -7,7 +7,7 @@ import pytest from hdk.common.compilation import CompilationConfiguration from hdk.common.data_types.integers import Integer -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.compile import compile_numpy_function_into_op_graph @@ -46,7 +46,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused op_graph = compile_numpy_function_into_op_graph( function_to_trace, { - param: EncryptedValue(Integer(32, is_signed=False)) + param: EncryptedScalar(Integer(32, is_signed=False)) for param in signature(function_to_trace).parameters.keys() }, iter([(1,), (2,), (3,)]), @@ -55,7 +55,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused op_graph_not_optimized = compile_numpy_function_into_op_graph( function_to_trace, { - param: EncryptedValue(Integer(32, is_signed=False)) + param: EncryptedScalar(Integer(32, is_signed=False)) for param in signature(function_to_trace).parameters.keys() }, iter([(1,), (2,), (3,)]), diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 51698bde7..64da36152 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -13,10 +13,10 @@ from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.values import ( BaseValue, + ClearScalar, ClearTensor, - ClearValue, + EncryptedScalar, EncryptedTensor, - EncryptedValue, ) @@ -24,14 +24,14 @@ from hdk.common.values import ( "value,expected_result", [ pytest.param( - ClearValue(Integer(8, is_signed=False)), + ClearScalar(Integer(8, is_signed=False)), False, - id="ClearValue 8 bits unsigned Integer", + id="ClearScalar 8 bits unsigned Integer", ), pytest.param( - EncryptedValue(Integer(8, is_signed=True)), + EncryptedScalar(Integer(8, is_signed=True)), True, - id="EncryptedValue 8 bits signed Integer", + id="EncryptedScalar 8 bits signed Integer", ), ], ) @@ -44,19 +44,19 @@ def test_value_is_encrypted_integer(value: BaseValue, expected_result: bool): "value,expected_result", [ pytest.param( - ClearValue(Integer(8, is_signed=False)), + ClearScalar(Integer(8, is_signed=False)), False, - id="ClearValue 8 bits unsigned Integer", + id="ClearScalar 8 bits unsigned Integer", ), pytest.param( - EncryptedValue(Integer(8, is_signed=True)), + EncryptedScalar(Integer(8, is_signed=True)), False, - id="EncryptedValue 8 bits signed Integer", + id="EncryptedScalar 8 bits signed Integer", ), pytest.param( - EncryptedValue(Integer(8, is_signed=False)), + EncryptedScalar(Integer(8, is_signed=False)), True, - id="EncryptedValue 8 bits unsigned Integer", + id="EncryptedScalar 8 bits unsigned Integer", ), ], ) @@ -136,39 +136,39 @@ def test_mix_data_types( "value1,value2,expected_mixed_value", [ pytest.param( - EncryptedValue(Integer(7, False)), - EncryptedValue(Integer(7, False)), - EncryptedValue(Integer(7, False)), + EncryptedScalar(Integer(7, False)), + EncryptedScalar(Integer(7, False)), + EncryptedScalar(Integer(7, False)), id="euint7, euint7, euint7", ), pytest.param( - EncryptedValue(Integer(7, False)), - ClearValue(Integer(7, False)), - EncryptedValue(Integer(7, False)), + EncryptedScalar(Integer(7, False)), + ClearScalar(Integer(7, False)), + EncryptedScalar(Integer(7, False)), id="euint7, cuint7, euint7", ), pytest.param( - ClearValue(Integer(7, False)), - EncryptedValue(Integer(7, False)), - EncryptedValue(Integer(7, False)), + ClearScalar(Integer(7, False)), + EncryptedScalar(Integer(7, False)), + EncryptedScalar(Integer(7, False)), id="cuint7, euint7, euint7", ), pytest.param( - ClearValue(Integer(7, False)), - ClearValue(Integer(7, False)), - ClearValue(Integer(7, False)), + ClearScalar(Integer(7, False)), + ClearScalar(Integer(7, False)), + ClearScalar(Integer(7, False)), id="cuint7, cuint7, cuint7", ), pytest.param( - ClearValue(Float(32)), - ClearValue(Float(32)), - ClearValue(Float(32)), + ClearScalar(Float(32)), + ClearScalar(Float(32)), + ClearScalar(Float(32)), id="cfloat32, cfloat32, cfloat32", ), pytest.param( - EncryptedValue(Float(32)), - ClearValue(Float(32)), - EncryptedValue(Float(32)), + EncryptedScalar(Float(32)), + ClearScalar(Float(32)), + EncryptedScalar(Float(32)), id="efloat32, cfloat32, efloat32", ), ], @@ -204,7 +204,7 @@ def test_mix_scalar_values(value1, value2, expected_mixed_value): ), pytest.param( ClearTensor(Integer(7, False), (1, 2, 3)), - EncryptedValue(Integer(7, False)), + EncryptedScalar(Integer(7, False)), None, marks=pytest.mark.xfail(raises=AssertionError), ), diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py index 1572996b9..03fae0069 100644 --- a/tests/common/debugging/test_drawing.py +++ b/tests/common/debugging/test_drawing.py @@ -5,7 +5,7 @@ from pathlib import Path from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.compile import compile_numpy_function_into_op_graph @@ -17,7 +17,7 @@ def test_draw_graph_with_saving(): op_graph = compile_numpy_function_into_op_graph( function, - {"x": EncryptedValue(Integer(7, True))}, + {"x": EncryptedScalar(Integer(7, True))}, iter([(-2,), (-1,), (0,), (1,), (2,)]), ) diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 7de22abe8..740e8e382 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -9,7 +9,7 @@ from hdk.common import is_a_power_of_2 from hdk.common.data_types.integers import Integer from hdk.common.extensions.table import LookupTable from hdk.common.representation import intermediate as ir -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy import tracing @@ -42,7 +42,7 @@ def test_lookup_table_encrypted_lookup(test_helpers): def f(x): return table[x] - x = EncryptedValue(Integer(2, is_signed=False)) + x = EncryptedScalar(Integer(2, is_signed=False)) op_graph = tracing.trace_numpy_function(f, {"x": x}) assert op_graph.output_nodes[0].get_table() == [3, 6, 0, 2] @@ -77,7 +77,7 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): def f(x): return table[x] + table[0] - x = EncryptedValue(Integer(3, is_signed=False)) + x = EncryptedScalar(Integer(3, is_signed=False)) op_graph = tracing.trace_numpy_function(f, {"x": x}) ref_graph = nx.MultiDiGraph() diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index c84c34f53..ffcf3aff5 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -4,7 +4,7 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.mlir.converters import add, apply_lut, constant, mul, sub -from hdk.common.values import ClearValue, EncryptedValue +from hdk.common.values import ClearScalar, EncryptedScalar class MockNode: @@ -31,21 +31,21 @@ def test_failing_converter(converter): def test_fail_non_integer_const(): """Test failing constant converter with non-integer""" with pytest.raises(TypeError, match=r"Don't support non-integer constants"): - constant(MockNode(outputs=[ClearValue(Float(32))]), None, None, None) + constant(MockNode(outputs=[ClearScalar(Float(32))]), None, None, None) def test_fail_signed_integer_const(): """Test failing constant converter with non-integer""" with pytest.raises(TypeError, match=r"Don't support signed constant integer"): - constant(MockNode(outputs=[ClearValue(Integer(8, True))]), None, None, None) + constant(MockNode(outputs=[ClearScalar(Integer(8, True))]), None, None, None) @pytest.mark.parametrize( "input_node", [ - ClearValue(Integer(8, True)), - ClearValue(Integer(8, False)), - EncryptedValue(Integer(8, True)), + ClearScalar(Integer(8, True)), + ClearScalar(Integer(8, False)), + EncryptedScalar(Integer(8, True)), ], ) def test_fail_tlu_input(input_node): @@ -54,7 +54,7 @@ def test_fail_tlu_input(input_node): TypeError, match=r"Only support LUT with encrypted unsigned integers inputs" ): apply_lut( - MockNode(inputs=[input_node], outputs=[EncryptedValue(Integer(8, False))]), + MockNode(inputs=[input_node], outputs=[EncryptedScalar(Integer(8, False))]), [None], None, None, @@ -64,9 +64,9 @@ def test_fail_tlu_input(input_node): @pytest.mark.parametrize( "input_node", [ - ClearValue(Integer(8, True)), - ClearValue(Integer(8, False)), - EncryptedValue(Integer(8, True)), + ClearScalar(Integer(8, True)), + ClearScalar(Integer(8, False)), + EncryptedScalar(Integer(8, True)), ], ) def test_fail_tlu_output(input_node): @@ -75,7 +75,7 @@ def test_fail_tlu_output(input_node): TypeError, match=r"Only support LUT with encrypted unsigned integers outputs" ): apply_lut( - MockNode(inputs=[EncryptedValue(Integer(8, False))], outputs=[input_node]), + MockNode(inputs=[EncryptedScalar(Integer(8, False))], outputs=[input_node]), [None], None, None, diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index b4fadaa42..65884812d 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -10,7 +10,7 @@ from zamalang.dialects import hlfhe from hdk.common.data_types.integers import Integer from hdk.common.extensions.table import LookupTable from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter -from hdk.common.values import ClearValue, EncryptedValue +from hdk.common.values import ClearScalar, EncryptedScalar from hdk.hnumpy.compile import compile_numpy_function_into_op_graph @@ -77,103 +77,103 @@ def datagen(*args): ( add, { - "x": EncryptedValue(Integer(64, is_signed=False)), - "y": ClearValue(Integer(32, is_signed=False)), + "x": EncryptedScalar(Integer(64, is_signed=False)), + "y": ClearScalar(Integer(32, is_signed=False)), }, (range(0, 8), range(1, 4)), ), ( constant_add, { - "x": EncryptedValue(Integer(64, is_signed=False)), + "x": EncryptedScalar(Integer(64, is_signed=False)), }, (range(0, 8),), ), ( add, { - "x": ClearValue(Integer(32, is_signed=False)), - "y": EncryptedValue(Integer(64, is_signed=False)), + "x": ClearScalar(Integer(32, is_signed=False)), + "y": EncryptedScalar(Integer(64, is_signed=False)), }, (range(0, 8), range(1, 4)), ), ( add, { - "x": EncryptedValue(Integer(7, is_signed=False)), - "y": EncryptedValue(Integer(7, is_signed=False)), + "x": EncryptedScalar(Integer(7, is_signed=False)), + "y": EncryptedScalar(Integer(7, is_signed=False)), }, (range(7, 15), range(1, 5)), ), ( sub, { - "x": ClearValue(Integer(8, is_signed=False)), - "y": EncryptedValue(Integer(7, is_signed=False)), + "x": ClearScalar(Integer(8, is_signed=False)), + "y": EncryptedScalar(Integer(7, is_signed=False)), }, (range(5, 10), range(2, 6)), ), ( constant_sub, { - "x": EncryptedValue(Integer(64, is_signed=False)), + "x": EncryptedScalar(Integer(64, is_signed=False)), }, (range(0, 5),), ), ( mul, { - "x": EncryptedValue(Integer(7, is_signed=False)), - "y": ClearValue(Integer(8, is_signed=False)), + "x": EncryptedScalar(Integer(7, is_signed=False)), + "y": ClearScalar(Integer(8, is_signed=False)), }, (range(1, 5), range(2, 8)), ), ( constant_mul, { - "x": EncryptedValue(Integer(64, is_signed=False)), + "x": EncryptedScalar(Integer(64, is_signed=False)), }, (range(0, 8),), ), ( mul, { - "x": ClearValue(Integer(8, is_signed=False)), - "y": EncryptedValue(Integer(7, is_signed=False)), + "x": ClearScalar(Integer(8, is_signed=False)), + "y": EncryptedScalar(Integer(7, is_signed=False)), }, (range(1, 5), range(2, 8)), ), ( sub_add_mul, { - "x": EncryptedValue(Integer(7, is_signed=False)), - "y": EncryptedValue(Integer(7, is_signed=False)), - "z": ClearValue(Integer(7, is_signed=False)), + "x": EncryptedScalar(Integer(7, is_signed=False)), + "y": EncryptedScalar(Integer(7, is_signed=False)), + "z": ClearScalar(Integer(7, is_signed=False)), }, (range(0, 8), range(1, 5), range(5, 12)), ), ( ret_multiple, { - "x": EncryptedValue(Integer(7, is_signed=False)), - "y": EncryptedValue(Integer(7, is_signed=False)), - "z": ClearValue(Integer(7, is_signed=False)), + "x": EncryptedScalar(Integer(7, is_signed=False)), + "y": EncryptedScalar(Integer(7, is_signed=False)), + "z": ClearScalar(Integer(7, is_signed=False)), }, (range(1, 5), range(1, 5), range(1, 5)), ), ( ret_multiple_different_order, { - "x": EncryptedValue(Integer(7, is_signed=False)), - "y": EncryptedValue(Integer(7, is_signed=False)), - "z": ClearValue(Integer(7, is_signed=False)), + "x": EncryptedScalar(Integer(7, is_signed=False)), + "y": EncryptedScalar(Integer(7, is_signed=False)), + "z": ClearScalar(Integer(7, is_signed=False)), }, (range(1, 5), range(1, 5), range(1, 5)), ), ( lut, { - "x": EncryptedValue(Integer(64, is_signed=False)), + "x": EncryptedScalar(Integer(64, is_signed=False)), }, (range(0, 8),), ), @@ -190,8 +190,8 @@ def test_mlir_converter(func, args_dict, args_ranges): def test_hdk_encrypted_integer_to_mlir_type(): - """Test conversion of EncryptedValue into MLIR""" - value = EncryptedValue(Integer(7, is_signed=False)) + """Test conversion of EncryptedScalar into MLIR""" + value = EncryptedScalar(Integer(7, is_signed=False)) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) eint = converter.hdk_value_to_mlir_type(value) assert eint == hlfhe.EncryptedIntegerType.get(converter.context, 7) @@ -199,8 +199,8 @@ def test_hdk_encrypted_integer_to_mlir_type(): @pytest.mark.parametrize("is_signed", [True, False]) def test_hdk_clear_integer_to_mlir_type(is_signed): - """Test conversion of ClearValue into MLIR""" - value = ClearValue(Integer(5, is_signed=is_signed)) + """Test conversion of ClearScalar into MLIR""" + value = ClearScalar(Integer(5, is_signed=is_signed)) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) int_mlir = converter.hdk_value_to_mlir_type(value) with converter.context: diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 9f1adccfb..2ceb70774 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -7,7 +7,7 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.optimization.topological import fuse_float_operations -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.tracing import trace_numpy_function @@ -89,7 +89,7 @@ def test_fuse_float_operations(function_to_trace, fused, input_): op_graph = trace_numpy_function( function_to_trace, - {param_name: EncryptedValue(Integer(32, True)) for param_name in params_names}, + {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, ) orig_num_nodes = len(op_graph.graph) fuse_float_operations(op_graph) diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 4b08d655c..dcab16b77 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -6,36 +6,36 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.representation import intermediate as ir -from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, EncryptedValue +from hdk.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor @pytest.mark.parametrize( "node,input_data,expected_result", [ pytest.param( - ir.Add([EncryptedValue(Integer(64, False)), EncryptedValue(Integer(64, False))]), + ir.Add([EncryptedScalar(Integer(64, False)), EncryptedScalar(Integer(64, False))]), [10, 4589], 4599, id="Add", ), pytest.param( - ir.Sub([EncryptedValue(Integer(64, False)), EncryptedValue(Integer(64, False))]), + ir.Sub([EncryptedScalar(Integer(64, False)), EncryptedScalar(Integer(64, False))]), [10, 4589], -4579, id="Sub", ), pytest.param( - ir.Mul([EncryptedValue(Integer(64, False)), EncryptedValue(Integer(64, False))]), + ir.Mul([EncryptedScalar(Integer(64, False)), EncryptedScalar(Integer(64, False))]), [10, 4589], 45890, id="Mul", ), - pytest.param(ir.Input(ClearValue(Integer(32, True)), "in", 0), [42], 42, id="Input"), + pytest.param(ir.Input(ClearScalar(Integer(32, True)), "in", 0), [42], 42, id="Input"), pytest.param(ir.Constant(42), None, 42, id="Constant"), pytest.param(ir.Constant(-42), None, -42, id="Constant"), pytest.param( ir.ArbitraryFunction( - EncryptedValue(Integer(7, False)), lambda x: x + 3, Integer(7, False) + EncryptedScalar(Integer(7, False)), lambda x: x + 3, Integer(7, False) ), [10], 13, @@ -43,7 +43,7 @@ from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, Encrypte ), pytest.param( ir.ArbitraryFunction( - EncryptedValue(Integer(7, False)), + EncryptedScalar(Integer(7, False)), lambda x, y: x + y, Integer(7, False), op_kwargs={"y": 3}, @@ -54,7 +54,7 @@ from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, Encrypte ), pytest.param( ir.ArbitraryFunction( - EncryptedValue(Integer(7, False)), + EncryptedScalar(Integer(7, False)), lambda x, y: y[x], Integer(7, False), op_kwargs={"y": (1, 2, 3, 4)}, @@ -65,7 +65,7 @@ from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, Encrypte ), pytest.param( ir.ArbitraryFunction( - EncryptedValue(Integer(7, False)), + EncryptedScalar(Integer(7, False)), lambda x, y: y[3], Integer(7, False), op_kwargs={"y": (1, 2, 3, 4)}, @@ -129,68 +129,68 @@ def test_evaluate( "node1,node2,expected_result", [ ( - ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), - ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Add([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), + ir.Add([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), True, ), ( - ir.Add([EncryptedValue(Integer(16, False)), EncryptedValue(Integer(32, False))]), - ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), + ir.Add([EncryptedScalar(Integer(16, False)), EncryptedScalar(Integer(32, False))]), + ir.Add([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(16, False))]), True, ), ( - ir.Add([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Add([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), False, ), ( - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), True, ), ( - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(16, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(16, False))]), True, ), ( - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(16, False))]), - ir.Sub([EncryptedValue(Integer(16, False)), EncryptedValue(Integer(32, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(16, False))]), + ir.Sub([EncryptedScalar(Integer(16, False)), EncryptedScalar(Integer(32, False))]), False, ), ( - ir.Mul([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), - ir.Mul([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Mul([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), + ir.Mul([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), True, ), ( - ir.Mul([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Mul([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), False, ), ( - ir.Input(EncryptedValue(Integer(32, False)), "x", 0), - ir.Sub([EncryptedValue(Integer(32, False)), EncryptedValue(Integer(32, False))]), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 0), + ir.Sub([EncryptedScalar(Integer(32, False)), EncryptedScalar(Integer(32, False))]), False, ), ( - ir.Input(EncryptedValue(Integer(32, False)), "x", 0), - ir.Input(EncryptedValue(Integer(32, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 0), True, ), ( - ir.Input(EncryptedValue(Integer(32, False)), "x", 0), - ir.Input(EncryptedValue(Integer(32, False)), "y", 0), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(32, False)), "y", 0), False, ), ( - ir.Input(EncryptedValue(Integer(32, False)), "x", 0), - ir.Input(EncryptedValue(Integer(32, False)), "x", 1), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 1), False, ), ( - ir.Input(EncryptedValue(Integer(32, False)), "x", 0), - ir.Input(EncryptedValue(Integer(8, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(32, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(8, False)), "x", 0), False, ), ( @@ -200,7 +200,7 @@ def test_evaluate( ), ( ir.Constant(10), - ir.Input(EncryptedValue(Integer(8, False)), "x", 0), + ir.Input(EncryptedScalar(Integer(8, False)), "x", 0), False, ), ( @@ -209,28 +209,36 @@ def test_evaluate( False, ), ( - ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), - ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + ir.ArbitraryFunction( + EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) + ), + ir.ArbitraryFunction( + EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) + ), True, ), ( ir.ArbitraryFunction( - EncryptedValue(Integer(8, False)), + EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False), op_args=(1, 2, 3), ), - ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + ir.ArbitraryFunction( + EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) + ), False, ), ( ir.ArbitraryFunction( - EncryptedValue(Integer(8, False)), + EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False), op_kwargs={"tuple": (1, 2, 3)}, ), - ir.ArbitraryFunction(EncryptedValue(Integer(8, False)), lambda x: x, Integer(8, False)), + ir.ArbitraryFunction( + EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) + ), False, ), ( diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index f2cf90a81..3ab3e5d4e 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -7,7 +7,7 @@ import pytest from hdk.common import check_op_graph_is_integer_program, is_a_power_of_2 from hdk.common.data_types.floats import Float64 from hdk.common.data_types.integers import Integer -from hdk.common.values import EncryptedValue +from hdk.common.values import EncryptedScalar from hdk.hnumpy.tracing import trace_numpy_function @@ -36,7 +36,7 @@ def test_check_op_graph_is_integer_program(): return x + y - y * y + x * y op_graph = trace_numpy_function( - function, {"x": EncryptedValue(Integer(64, True)), "y": EncryptedValue(Integer(64, True))} + function, {"x": EncryptedScalar(Integer(64, True)), "y": EncryptedScalar(Integer(64, True))} ) # Test without and with output list diff --git a/tests/hnumpy/test_compile.py b/tests/hnumpy/test_compile.py index e1d98ed30..7c2fa862b 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/hnumpy/test_compile.py @@ -9,7 +9,7 @@ from hdk.common.compilation import CompilationConfiguration from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable -from hdk.common.values import EncryptedTensor, EncryptedValue +from hdk.common.values import EncryptedScalar, EncryptedTensor from hdk.hnumpy.compile import ( compile_numpy_function, compile_numpy_function_into_op_graph, @@ -57,7 +57,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n yield prod function_parameters = { - arg_name: EncryptedValue(Integer(64, True)) for arg_name in list_of_arg_names + arg_name: EncryptedScalar(Integer(64, True)) for arg_name in list_of_arg_names } op_graph = compile_numpy_function_into_op_graph( @@ -94,7 +94,7 @@ def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_ yield prod function_parameters = { - arg_name: EncryptedValue(Integer(64, False)) for arg_name in list_of_arg_names + arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names } compiler_engine = compile_numpy_function( @@ -126,7 +126,7 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): yield prod function_parameters = { - arg_name: EncryptedValue(Integer(64, False)) for arg_name in list_of_arg_names + arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names } compiler_engine = compile_numpy_function( @@ -149,7 +149,7 @@ def test_compile_function_with_direct_tlu(): op_graph = compile_numpy_function_into_op_graph( function, - {"x": EncryptedValue(Integer(2, is_signed=False))}, + {"x": EncryptedScalar(Integer(2, is_signed=False))}, iter([(0,), (1,), (2,), (3,)]), ) @@ -168,7 +168,7 @@ def test_compile_function_with_direct_tlu_overflow(): with pytest.raises(ValueError): compile_numpy_function_into_op_graph( function, - {"x": EncryptedValue(Integer(3, is_signed=False))}, + {"x": EncryptedScalar(Integer(3, is_signed=False))}, iter([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)]), CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) @@ -188,7 +188,7 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): yield prod function_parameters = { - arg_name: EncryptedValue(Integer(64, True)) for arg_name in list_of_arg_names + arg_name: EncryptedScalar(Integer(64, True)) for arg_name in list_of_arg_names } with pytest.raises(TypeError, match=r"signed integers aren't supported for MLIR lowering"): @@ -268,7 +268,7 @@ def test_compile_with_show_mlir(function, input_ranges, list_of_arg_names): yield prod function_parameters = { - arg_name: EncryptedValue(Integer(64, False)) for arg_name in list_of_arg_names + arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names } compile_numpy_function( diff --git a/tests/hnumpy/test_debugging.py b/tests/hnumpy/test_debugging.py index e69bd63fb..720878588 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/hnumpy/test_debugging.py @@ -6,7 +6,7 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable -from hdk.common.values import ClearValue, EncryptedTensor, EncryptedValue +from hdk.common.values import ClearScalar, EncryptedScalar, EncryptedTensor from hdk.hnumpy import tracing LOOKUP_TABLE_FROM_2B_TO_4B = LookupTable([9, 2, 4, 11]) @@ -119,15 +119,15 @@ def issue_130_c(x, y): [ pytest.param( ( - EncryptedValue(Integer(64, is_signed=False)), - EncryptedValue(Integer(64, is_signed=False)), + EncryptedScalar(Integer(64, is_signed=False)), + EncryptedScalar(Integer(64, is_signed=False)), ), id="Encrypted uint", ), pytest.param( ( - EncryptedValue(Integer(64, is_signed=False)), - ClearValue(Integer(64, is_signed=False)), + EncryptedScalar(Integer(64, is_signed=False)), + ClearScalar(Integer(64, is_signed=False)), ), id="Clear uint", ), @@ -154,12 +154,12 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): [ ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], - {"x": EncryptedValue(Integer(2, is_signed=False))}, + {"x": EncryptedScalar(Integer(2, is_signed=False))}, "%0 = x\n%1 = TLU(0)\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], - {"x": EncryptedValue(Integer(2, is_signed=False))}, + {"x": EncryptedScalar(Integer(2, is_signed=False))}, "%0 = x\n%1 = Constant(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)\n", ), ], @@ -218,8 +218,8 @@ def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): ( lambda x, y: x + y, ( - EncryptedValue(Integer(64, is_signed=False)), - EncryptedValue(Integer(32, is_signed=True)), + EncryptedScalar(Integer(64, is_signed=False)), + EncryptedScalar(Integer(32, is_signed=True)), ), "%0 = x # Integer" "\n%1 = y # Integer" @@ -229,8 +229,8 @@ def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): ( lambda x, y: x * y, ( - EncryptedValue(Integer(17, is_signed=False)), - EncryptedValue(Integer(23, is_signed=False)), + EncryptedScalar(Integer(17, is_signed=False)), + EncryptedScalar(Integer(23, is_signed=False)), ), "%0 = x # Integer" "\n%1 = y # Integer" @@ -258,14 +258,14 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): [ ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], - {"x": EncryptedValue(Integer(2, is_signed=False))}, + {"x": EncryptedScalar(Integer(2, is_signed=False))}, "%0 = x # Integer" "\n%1 = TLU(0) # Integer" "\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], - {"x": EncryptedValue(Integer(2, is_signed=False))}, + {"x": EncryptedScalar(Integer(2, is_signed=False))}, "%0 = x # Integer" "\n%1 = Constant(4) # Integer" "\n%2 = Add(0, 1) # Integer" @@ -274,7 +274,7 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): ), ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[LOOKUP_TABLE_FROM_3B_TO_2B[x + 4]], - {"x": EncryptedValue(Integer(2, is_signed=False))}, + {"x": EncryptedScalar(Integer(2, is_signed=False))}, "%0 = x # Integer" "\n%1 = Constant(4) # Integer" "\n%2 = Add(0, 1) # Integer" diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 7bc4a4538..32d8e822f 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -7,7 +7,7 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.representation import intermediate as ir -from hdk.common.values import ClearTensor, ClearValue, EncryptedTensor, EncryptedValue +from hdk.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor from hdk.hnumpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] @@ -20,17 +20,17 @@ OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] @pytest.mark.parametrize( "x", [ - pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="x: Encrypted uint"), + pytest.param(EncryptedScalar(Integer(64, is_signed=False)), id="x: Encrypted uint"), pytest.param( - EncryptedValue(Integer(64, is_signed=True)), + EncryptedScalar(Integer(64, is_signed=True)), id="x: Encrypted int", ), pytest.param( - ClearValue(Integer(64, is_signed=False)), + ClearScalar(Integer(64, is_signed=False)), id="x: Clear uint", ), pytest.param( - ClearValue(Integer(64, is_signed=True)), + ClearScalar(Integer(64, is_signed=True)), id="x: Clear int", ), ], @@ -38,17 +38,17 @@ OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] @pytest.mark.parametrize( "y", [ - pytest.param(EncryptedValue(Integer(64, is_signed=False)), id="y: Encrypted uint"), + pytest.param(EncryptedScalar(Integer(64, is_signed=False)), id="y: Encrypted uint"), pytest.param( - EncryptedValue(Integer(64, is_signed=True)), + EncryptedScalar(Integer(64, is_signed=True)), id="y: Encrypted int", ), pytest.param( - ClearValue(Integer(64, is_signed=False)), + ClearScalar(Integer(64, is_signed=False)), id="y: Clear uint", ), pytest.param( - ClearValue(Integer(64, is_signed=True)), + ClearScalar(Integer(64, is_signed=True)), id="y: Clear int", ), ], @@ -215,9 +215,9 @@ def test_tracing_astype( """Test function for NPTracer.astype""" for input_, expected_output in input_and_expected_output_tuples: input_value = ( - EncryptedValue(Integer(64, is_signed=True)) + EncryptedScalar(Integer(64, is_signed=True)) if isinstance(input_, int) - else EncryptedValue(Float(64)) + else EncryptedScalar(Float(64)) ) op_graph = tracing.trace_numpy_function(function_to_trace, {"x": input_value}) @@ -259,30 +259,30 @@ def test_tracing_astype( "inputs,expected_output_node,expected_output_value", [ pytest.param( - {"x": EncryptedValue(Integer(7, is_signed=False))}, + {"x": EncryptedScalar(Integer(7, is_signed=False))}, ir.ArbitraryFunction, - EncryptedValue(Float(64)), + EncryptedScalar(Float(64)), ), pytest.param( - {"x": EncryptedValue(Integer(32, is_signed=True))}, + {"x": EncryptedScalar(Integer(32, is_signed=True))}, ir.ArbitraryFunction, - EncryptedValue(Float(64)), + EncryptedScalar(Float(64)), ), pytest.param( - {"x": EncryptedValue(Integer(64, is_signed=True))}, + {"x": EncryptedScalar(Integer(64, is_signed=True))}, ir.ArbitraryFunction, - EncryptedValue(Float(64)), + EncryptedScalar(Float(64)), ), pytest.param( - {"x": EncryptedValue(Integer(128, is_signed=True))}, + {"x": EncryptedScalar(Integer(128, is_signed=True))}, ir.ArbitraryFunction, None, marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), ), pytest.param( - {"x": EncryptedValue(Float(64))}, + {"x": EncryptedScalar(Float(64))}, ir.ArbitraryFunction, - EncryptedValue(Float(64)), + EncryptedScalar(Float(64)), ), ], ) @@ -309,7 +309,7 @@ def test_trace_hnumpy_supported_ufuncs( "y": EncryptedTensor(Integer(7, is_signed=False), shape=(10,)), }, ir.Dot, - EncryptedValue(Integer(32, False)), + EncryptedScalar(Integer(32, False)), ), pytest.param( lambda x, y: numpy.dot(x, y), @@ -318,7 +318,7 @@ def test_trace_hnumpy_supported_ufuncs( "y": EncryptedTensor(Float(64), shape=(10,)), }, ir.Dot, - EncryptedValue(Float(64)), + EncryptedScalar(Float(64)), ), pytest.param( lambda x, y: numpy.dot(x, y), @@ -327,7 +327,7 @@ def test_trace_hnumpy_supported_ufuncs( "y": ClearTensor(Integer(64, is_signed=True), shape=(6,)), }, ir.Dot, - ClearValue(Integer(64, is_signed=True)), + ClearScalar(Integer(64, is_signed=True)), ), pytest.param( lambda x: numpy.dot(x, numpy.array([1, 2, 3, 4, 5], dtype=numpy.int64)), @@ -335,7 +335,7 @@ def test_trace_hnumpy_supported_ufuncs( "x": EncryptedTensor(Integer(64, is_signed=True), shape=(5,)), }, ir.Dot, - EncryptedValue(Integer(64, True)), + EncryptedScalar(Integer(64, True)), ), # pylint: enable=unnecessary-lambda ], @@ -381,7 +381,7 @@ def test_nptracer_get_tracing_func_for_np_functions(np_function, expected_tracin @pytest.mark.parametrize( "tracer", [ - tracing.NPTracer([], ir.Input(ClearValue(Integer(32, True)), "x", 0), 0), + tracing.NPTracer([], ir.Input(ClearScalar(Integer(32, True)), "x", 0), 0), ], ) @pytest.mark.parametrize( From b582e68cd0e7617a38a6b26dd6e4a1b2edd0e85f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 1 Sep 2021 11:45:51 +0200 Subject: [PATCH 0173/1104] dev: add OPGraphs to compilation artifacts during float fusing --- hdk/common/optimization/topological.py | 16 +++++++++++++++- hdk/hnumpy/compile.py | 5 +---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/hdk/common/optimization/topological.py b/hdk/common/optimization/topological.py index e374ca57e..cc66bb731 100644 --- a/hdk/common/optimization/topological.py +++ b/hdk/common/optimization/topological.py @@ -4,21 +4,29 @@ from typing import Dict, List, Optional, Set, Tuple import networkx as nx +from ..compilation.artifacts import CompilationArtifacts from ..data_types.floats import Float from ..data_types.integers import Integer from ..operator_graph import OPGraph from ..representation import intermediate as ir -def fuse_float_operations(op_graph: OPGraph): +def fuse_float_operations( + op_graph: OPGraph, + compilation_artifacts: Optional[CompilationArtifacts] = None, +): """Finds and fuses float domains into single Integer to Integer ArbitraryFunction. Args: op_graph (OPGraph): The OPGraph to simplify + compilation_artifacts (Optional[CompilationArtifacts]): The CompilationArtifacts of the + current compilation, this argument is optional as it's not required to execute float + fusing. """ nx_graph = op_graph.graph processed_terminal_nodes: Set[ir.IntermediateNode] = set() + number_of_fuse = 0 while True: float_subgraph_search_result = find_float_subgraph_with_unique_terminal_node( nx_graph, processed_terminal_nodes @@ -68,6 +76,12 @@ def fuse_float_operations(op_graph: OPGraph): nx_graph.add_edge(node_before_subgraph, fused_node, input_idx=0) op_graph.prune_nodes() + if compilation_artifacts is not None: + compilation_artifacts.add_operation_graph( + f"after-float-fuse-{number_of_fuse}", op_graph + ) + + number_of_fuse += 1 def convert_float_subgraph_to_fused_node( diff --git a/hdk/hnumpy/compile.py b/hdk/hnumpy/compile.py index 51208b96f..334490cb6 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/hnumpy/compile.py @@ -90,10 +90,7 @@ def _compile_numpy_function_into_op_graph_internal( if compilation_configuration.enable_topological_optimizations: # Fuse float operations to have int to int ArbitraryFunction if not check_op_graph_is_integer_program(op_graph): - fuse_float_operations(op_graph) - - # Add the fused floats graph as an artifact - compilation_artifacts.add_operation_graph("fused-float-operations", op_graph) + fuse_float_operations(op_graph, compilation_artifacts) # TODO: To be removed once we support more than integers offending_non_integer_nodes: List[ir.IntermediateNode] = [] From e9c5ce27bb8ff327fa0a6a0054acd5bc90f783aa Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 31 Aug 2021 17:28:52 +0200 Subject: [PATCH 0174/1104] feat: factorize the ufunc management and add lot of ufunc's refs #126 --- hdk/hnumpy/tracing.py | 215 +++++++++++++++++++---------------- tests/hnumpy/test_tracing.py | 105 ++++++++--------- 2 files changed, 166 insertions(+), 154 deletions(-) diff --git a/hdk/hnumpy/tracing.py b/hdk/hnumpy/tracing.py index bb31aa746..4776278fb 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/hnumpy/tracing.py @@ -1,7 +1,7 @@ """hnumpy tracing utilities.""" from copy import deepcopy from functools import partial -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union import numpy from numpy.typing import DTypeLike @@ -43,7 +43,7 @@ class NPTracer(BaseTracer): assert ( len(kwargs) == 0 ), f"hnumpy does not support **kwargs currently for numpy ufuncs, ufunc: {ufunc}" - return tracing_func(self, *input_tracers, **kwargs) + return tracing_func(*input_tracers, **kwargs) raise NotImplementedError("Only __call__ method is supported currently") def __array_function__(self, func, _types, args, kwargs): @@ -130,8 +130,9 @@ class NPTracer(BaseTracer): ] return common_output_dtypes + @classmethod def _unary_operator( - self, unary_operator, unary_operator_string, *input_tracers: "NPTracer", **kwargs + cls, unary_operator, unary_operator_string, *input_tracers: "NPTracer", **kwargs ) -> "NPTracer": """Function to trace an unary operator. @@ -139,7 +140,7 @@ class NPTracer(BaseTracer): NPTracer: The output NPTracer containing the traced function """ assert len(input_tracers) == 1 - common_output_dtypes = self._manage_dtypes(unary_operator, *input_tracers) + common_output_dtypes = cls._manage_dtypes(unary_operator, *input_tracers) assert len(common_output_dtypes) == 1 traced_computation = ArbitraryFunction( @@ -149,93 +150,13 @@ class NPTracer(BaseTracer): op_kwargs=deepcopy(kwargs), op_name=unary_operator_string, ) - output_tracer = self.__class__( + output_tracer = cls( input_tracers, traced_computation=traced_computation, output_index=0, ) return output_tracer - def rint(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.rint. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.rint, "np.rint", *input_tracers, **kwargs) - - def sin(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.sin. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.sin, "np.sin", *input_tracers, **kwargs) - - def cos(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.cos. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.cos, "np.cos", *input_tracers, **kwargs) - - def tan(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.tan. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.tan, "np.tan", *input_tracers, **kwargs) - - def arcsin(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.arcsin. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.arcsin, "np.arcsin", *input_tracers, **kwargs) - - def arccos(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.arccos. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.arccos, "np.arccos", *input_tracers, **kwargs) - - def arctan(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.arctan. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.arctan, "np.arctan", *input_tracers, **kwargs) - - def exp(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.exp. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.exp, "np.exp", *input_tracers, **kwargs) - - def expm1(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.expm1. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.expm1, "np.expm1", *input_tracers, **kwargs) - - def exp2(self, *input_tracers: "NPTracer", **kwargs) -> "NPTracer": - """Function to trace numpy.exp2. - - Returns: - NPTracer: The output NPTracer containing the traced function - """ - return self._unary_operator(numpy.exp2, "np.exp2", *input_tracers, **kwargs) - def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": """Function to trace numpy.dot. @@ -261,24 +182,124 @@ class NPTracer(BaseTracer): ) return output_tracer - UFUNC_ROUTING: Dict[numpy.ufunc, Callable] = { - numpy.rint: rint, - numpy.sin: sin, - numpy.cos: cos, - numpy.tan: tan, - numpy.arcsin: arcsin, - numpy.arccos: arccos, - numpy.arctan: arctan, - numpy.exp: exp, - numpy.expm1: expm1, - numpy.exp2: exp2, - } + LIST_OF_SUPPORTED_UFUNC: List[numpy.ufunc] = [ + # The commented functions are functions which don't work for the moment, often + # if not always because they require more than a single argument + # numpy.absolute, + # numpy.add, + numpy.arccos, + numpy.arccosh, + numpy.arcsin, + numpy.arcsinh, + numpy.arctan, + # numpy.arctan2, + numpy.arctanh, + # numpy.bitwise_and, + # numpy.bitwise_or, + # numpy.bitwise_xor, + numpy.cbrt, + numpy.ceil, + # numpy.conjugate, + # numpy.copysign, + numpy.cos, + numpy.cosh, + numpy.deg2rad, + numpy.degrees, + # numpy.divmod, + # numpy.equal, + numpy.exp, + numpy.exp2, + numpy.expm1, + numpy.fabs, + # numpy.float_power, + numpy.floor, + # numpy.floor_divide, + # numpy.fmax, + # numpy.fmin, + # numpy.fmod, + # numpy.frexp, + # numpy.gcd, + # numpy.greater, + # numpy.greater_equal, + # numpy.heaviside, + # numpy.hypot, + # numpy.invert, + # numpy.isfinite, + # numpy.isinf, + # numpy.isnan, + # numpy.isnat, + # numpy.lcm, + # numpy.ldexp, + # numpy.left_shift, + # numpy.less, + # numpy.less_equal, + numpy.log, + numpy.log10, + numpy.log1p, + numpy.log2, + # numpy.logaddexp, + # numpy.logaddexp2, + # numpy.logical_and, + # numpy.logical_not, + # numpy.logical_or, + # numpy.logical_xor, + # numpy.matmul, + # numpy.maximum, + # numpy.minimum, + # numpy.modf, + # numpy.multiply, + # numpy.negative, + # numpy.nextafter, + # numpy.not_equal, + # numpy.positive, + # numpy.power, + numpy.rad2deg, + numpy.radians, + # numpy.reciprocal, + # numpy.remainder, + # numpy.right_shift, + numpy.rint, + # numpy.sign, + # numpy.signbit, + numpy.sin, + numpy.sinh, + numpy.spacing, + numpy.sqrt, + # numpy.square, + # numpy.subtract, + numpy.tan, + numpy.tanh, + # numpy.true_divide, + numpy.trunc, + ] + + # We build UFUNC_ROUTING dynamically after the creation of the class, + # because of some limits of python or our unability to do it properly + # in the class with techniques which are compatible with the different + # coding checks we use + UFUNC_ROUTING: Dict[numpy.ufunc, Callable] = {} FUNC_ROUTING: Dict[Callable, Callable] = { numpy.dot: dot, } +def _get_fun(function: numpy.ufunc): + """Helper function to wrap _unary_operator in a lambda to populate NPTRACER.UFUNC_ROUTING.""" + + # We have to access this method to be able to build NPTracer.UFUNC_ROUTING + # dynamically + # pylint: disable=protected-access + return lambda *input_tracers, **kwargs: NPTracer._unary_operator( + function, f"np.{function.__name__}", *input_tracers, **kwargs + ) + # pylint: enable=protected-access + + +# We are populating NPTracer.UFUNC_ROUTING dynamically +NPTracer.UFUNC_ROUTING = {fun: _get_fun(fun) for fun in NPTracer.LIST_OF_SUPPORTED_UFUNC} + + def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] ) -> OPGraph: diff --git a/tests/hnumpy/test_tracing.py b/tests/hnumpy/test_tracing.py index 32d8e822f..a16223260 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/hnumpy/test_tracing.py @@ -230,31 +230,6 @@ def test_tracing_astype( assert expected_output == evaluated_output -@pytest.mark.parametrize( - "function_to_trace", - [ - # We cannot call trace_numpy_function on some numpy function as getting the signature for - # these functions fails, so we wrap it in a lambda - # pylint: disable=unnecessary-lambda - pytest.param(lambda x: numpy.rint(x)), - pytest.param(lambda x: numpy.sin(x)), - pytest.param(lambda x: numpy.cos(x)), - pytest.param(lambda x: numpy.tan(x)), - pytest.param(lambda x: numpy.arcsin(x)), - pytest.param(lambda x: numpy.arccos(x)), - pytest.param(lambda x: numpy.arctan(x)), - pytest.param(lambda x: numpy.exp(x)), - pytest.param(lambda x: numpy.expm1(x)), - pytest.param(lambda x: numpy.exp2(x)), - # The next test case is only for coverage purposes, to trigger the unsupported method - # exception handling - pytest.param( - lambda x: numpy.add.reduce(x), - marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), - ), - # pylint: enable=unnecessary-lambda - ], -) @pytest.mark.parametrize( "inputs,expected_output_node,expected_output_value", [ @@ -286,16 +261,40 @@ def test_tracing_astype( ), ], ) -def test_trace_hnumpy_supported_ufuncs( - function_to_trace, inputs, expected_output_node, expected_output_value -): +def test_trace_hnumpy_supported_ufuncs(inputs, expected_output_node, expected_output_value): """Function to trace supported numpy ufuncs""" - op_graph = tracing.trace_numpy_function(function_to_trace, inputs) + for function_to_trace_def in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: - assert len(op_graph.output_nodes) == 1 - assert isinstance(op_graph.output_nodes[0], expected_output_node) - assert len(op_graph.output_nodes[0].outputs) == 1 - assert op_graph.output_nodes[0].outputs[0] == expected_output_value + # We really need a lambda (because numpy functions are not playing + # nice with inspect.signature), but pylint and flake8 are not happy + # with it + # pylint: disable=unnecessary-lambda,cell-var-from-loop + function_to_trace = lambda x: function_to_trace_def(x) # noqa: E731 + # pylint: enable=unnecessary-lambda,cell-var-from-loop + + op_graph = tracing.trace_numpy_function(function_to_trace, inputs) + + assert len(op_graph.output_nodes) == 1 + assert isinstance(op_graph.output_nodes[0], expected_output_node) + assert len(op_graph.output_nodes[0].outputs) == 1 + assert op_graph.output_nodes[0].outputs[0] == expected_output_value + + +def test_trace_hnumpy_ufuncs_not_supported(): + """Testing a failure case of trace_numpy_function""" + inputs = {"x": EncryptedScalar(Integer(128, is_signed=True))} + + # We really need a lambda (because numpy functions are not playing + # nice with inspect.signature), but pylint and flake8 are not happy + # with it + # pylint: disable=unnecessary-lambda + function_to_trace = lambda x: numpy.add.reduce(x) # noqa: E731 + # pylint: enable=unnecessary-lambda + + with pytest.raises(NotImplementedError) as excinfo: + tracing.trace_numpy_function(function_to_trace, inputs) + + assert "Only __call__ method is supported currently" in str(excinfo.value) @pytest.mark.parametrize( @@ -351,31 +350,23 @@ def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expec assert op_graph.output_nodes[0].outputs[0] == expected_output_value -@pytest.mark.parametrize( - "np_function,expected_tracing_func", - [ - pytest.param(numpy.rint, tracing.NPTracer.rint), - pytest.param(numpy.sin, tracing.NPTracer.sin), - pytest.param(numpy.cos, tracing.NPTracer.cos), - pytest.param(numpy.tan, tracing.NPTracer.tan), - pytest.param(numpy.arcsin, tracing.NPTracer.arcsin), - pytest.param(numpy.arccos, tracing.NPTracer.arccos), - pytest.param(numpy.arctan, tracing.NPTracer.arctan), - pytest.param(numpy.exp, tracing.NPTracer.exp), - pytest.param(numpy.expm1, tracing.NPTracer.expm1), - pytest.param(numpy.exp2, tracing.NPTracer.exp2), - pytest.param(numpy.dot, tracing.NPTracer.dot), - # There is a need to test the case where the function fails, I chose numpy.conjugate which - # works on complex types, as we don't talk about complex types for now this looks like a - # good long term candidate to check for an unsupported function - pytest.param( - numpy.conjugate, None, marks=pytest.mark.xfail(strict=True, raises=NotImplementedError) - ), - ], -) -def test_nptracer_get_tracing_func_for_np_functions(np_function, expected_tracing_func): +def test_nptracer_get_tracing_func_for_np_functions(): """Test NPTracer get_tracing_func_for_np_function""" - assert tracing.NPTracer.get_tracing_func_for_np_function(np_function) == expected_tracing_func + + for np_function in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: + expected_tracing_func = tracing.NPTracer.UFUNC_ROUTING[np_function] + + assert ( + tracing.NPTracer.get_tracing_func_for_np_function(np_function) == expected_tracing_func + ) + + +def test_nptracer_get_tracing_func_for_np_functions_not_implemented(): + """Check NPTracer in case of not-implemented function""" + with pytest.raises(NotImplementedError) as excinfo: + tracing.NPTracer.get_tracing_func_for_np_function(numpy.conjugate) + + assert "NPTracer does not yet manage the following func: conjugate" in str(excinfo.value) @pytest.mark.parametrize( From bf2585ba0a503cc0bc743e9040d9c5a645c5da66 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 1 Sep 2021 14:05:41 +0200 Subject: [PATCH 0175/1104] refacto: rename hnumpy to numpy as requested for the package imports --- benchmarks/test_compilation_and_evaluation.py | 2 +- docs/dev/COMPILATION.md | 2 +- docs/dev/FLOAT-FUSING.md | 2 +- docs/dev/GETTING-STARTED.md | 2 +- examples/QuantizedLinearRegression.ipynb | 2 +- examples/QuantizedLogisticRegression.ipynb | 2 +- hdk/__init__.py | 2 +- hdk/{hnumpy => numpy}/__init__.py | 0 hdk/{hnumpy => numpy}/compile.py | 8 +++---- hdk/{hnumpy => numpy}/np_dtypes_helpers.py | 0 hdk/{hnumpy => numpy}/tracing.py | 4 ++-- .../bounds_measurement/test_dataset_eval.py | 2 +- tests/common/compilation/test_artifacts.py | 2 +- .../common/compilation/test_configuration.py | 2 +- tests/common/debugging/test_drawing.py | 2 +- tests/common/extensions/test_table.py | 2 +- tests/common/mlir/test_mlir_converter.py | 2 +- .../common/optimization/test_float_fusing.py | 2 +- tests/common/test_common_helpers.py | 2 +- tests/{hnumpy => numpy}/test_compile.py | 4 ++-- tests/{hnumpy => numpy}/test_debugging.py | 24 +++++++++---------- .../test_np_dtypes_helpers.py | 4 ++-- tests/{hnumpy => numpy}/test_tracing.py | 16 ++++++------- 23 files changed, 45 insertions(+), 45 deletions(-) rename hdk/{hnumpy => numpy}/__init__.py (100%) rename hdk/{hnumpy => numpy}/compile.py (98%) rename hdk/{hnumpy => numpy}/np_dtypes_helpers.py (100%) rename hdk/{hnumpy => numpy}/tracing.py (98%) rename tests/{hnumpy => numpy}/test_compile.py (99%) rename tests/{hnumpy => numpy}/test_debugging.py (92%) rename tests/{hnumpy => numpy}/test_np_dtypes_helpers.py (96%) rename tests/{hnumpy => numpy}/test_tracing.py (96%) diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py index e2c3e4c9e..6e9b9496d 100644 --- a/benchmarks/test_compilation_and_evaluation.py +++ b/benchmarks/test_compilation_and_evaluation.py @@ -6,7 +6,7 @@ import pytest from hdk.common.data_types.integers import SignedInteger, UnsignedInteger from hdk.common.values import EncryptedScalar -from hdk.hnumpy.compile import compile_numpy_function_into_op_graph +from hdk.numpy.compile import compile_numpy_function_into_op_graph @pytest.mark.parametrize( diff --git a/docs/dev/COMPILATION.md b/docs/dev/COMPILATION.md index df07b2de3..876eefaf6 100644 --- a/docs/dev/COMPILATION.md +++ b/docs/dev/COMPILATION.md @@ -13,7 +13,7 @@ However, one can already build interesting and impressing use cases, and more wi # Import necessary HDK components from hdk.common.data_types.integers import UnsignedInteger from hdk.common.values import EncryptedScalar, EncryptedTensor -from hdk.hnumpy.compile import compile_numpy_function +from hdk.numpy.compile import compile_numpy_function # Define the function to homomorphize def f(x, y): diff --git a/docs/dev/FLOAT-FUSING.md b/docs/dev/FLOAT-FUSING.md index 8a981a1cb..cd5140aa8 100644 --- a/docs/dev/FLOAT-FUSING.md +++ b/docs/dev/FLOAT-FUSING.md @@ -4,7 +4,7 @@ The current compiler stack only supports integers with 7 bits or less. But it's not uncommon to have numpy code using floating point numbers. -We added fusing floating point operations to make hnumpy somewhat user friendly to allow in-line quantization in the numpy code e.g.: +We added fusing floating point operations to make tracing numpy functions somewhat user friendly to allow in-line quantization in the numpy code e.g.: ```python import numpy diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index 976287919..c7862b9da 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -106,7 +106,7 @@ In this section, we will discuss the module structure of hdk briefly. You are en - extensions: utilities that provide special functionality to our users - representation: type definitions of intermediate representation - tracing: utilities for generic function tracing used during intermediate representation creation - - hnumpy: numpy frontend of hdk + - numpy: numpy frontend of hdk ## Working in Docker diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 191a0618e..5ecdaaedd 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -623,7 +623,7 @@ "source": [ "from hdk.common.data_types.integers import Integer\n", "from hdk.common.values import EncryptedScalar\n", - "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", + "from hdk.numpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", "for x_i in x_q:\n", diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 9dd173755..3da58515c 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -727,7 +727,7 @@ "source": [ "from hdk.common.data_types.integers import Integer\n", "from hdk.common.values import EncryptedScalar\n", - "from hdk.hnumpy.compile import compile_numpy_function_into_op_graph\n", + "from hdk.numpy.compile import compile_numpy_function_into_op_graph\n", "\n", "dataset = []\n", "for x_i in x_q:\n", diff --git a/hdk/__init__.py b/hdk/__init__.py index 983f90088..dd0d3d4c0 100644 --- a/hdk/__init__.py +++ b/hdk/__init__.py @@ -1,2 +1,2 @@ """Package top import.""" -from . import common, hnumpy +from . import common, numpy diff --git a/hdk/hnumpy/__init__.py b/hdk/numpy/__init__.py similarity index 100% rename from hdk/hnumpy/__init__.py rename to hdk/numpy/__init__.py diff --git a/hdk/hnumpy/compile.py b/hdk/numpy/compile.py similarity index 98% rename from hdk/hnumpy/compile.py rename to hdk/numpy/compile.py index 334490cb6..f3700a1fc 100644 --- a/hdk/hnumpy/compile.py +++ b/hdk/numpy/compile.py @@ -1,4 +1,4 @@ -"""hnumpy compilation function.""" +"""numpy compilation function.""" import traceback from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple @@ -18,7 +18,7 @@ from ..common.operator_graph import OPGraph from ..common.optimization.topological import fuse_float_operations from ..common.representation import intermediate as ir from ..common.values import BaseValue -from ..hnumpy.tracing import trace_numpy_function +from ..numpy.tracing import trace_numpy_function from .np_dtypes_helpers import get_base_data_type_for_numpy_or_python_constant_data @@ -199,7 +199,7 @@ def _compile_numpy_function_internal( compilation_artifacts: CompilationArtifacts, show_mlir: bool, ) -> CompilerEngine: - """Main API of hnumpy, to be able to compile an homomorphic program. + """Internal part of the API to be able to compile an homomorphic program. Args: function_to_compile (Callable): The function you want to compile @@ -254,7 +254,7 @@ def compile_numpy_function( compilation_artifacts: Optional[CompilationArtifacts] = None, show_mlir: bool = False, ) -> CompilerEngine: - """Main API of hnumpy, to be able to compile an homomorphic program. + """Main API to be able to compile an homomorphic program. Args: function_to_compile (Callable): The function to compile diff --git a/hdk/hnumpy/np_dtypes_helpers.py b/hdk/numpy/np_dtypes_helpers.py similarity index 100% rename from hdk/hnumpy/np_dtypes_helpers.py rename to hdk/numpy/np_dtypes_helpers.py diff --git a/hdk/hnumpy/tracing.py b/hdk/numpy/tracing.py similarity index 98% rename from hdk/hnumpy/tracing.py rename to hdk/numpy/tracing.py index 4776278fb..d63b13b16 100644 --- a/hdk/hnumpy/tracing.py +++ b/hdk/numpy/tracing.py @@ -1,4 +1,4 @@ -"""hnumpy tracing utilities.""" +"""numpy tracing utilities.""" from copy import deepcopy from functools import partial from typing import Any, Callable, Dict, List, Optional, Union @@ -54,7 +54,7 @@ class NPTracer(BaseTracer): tracing_func = self.get_tracing_func_for_np_function(func) assert ( len(kwargs) == 0 - ), f"hnumpy does not support **kwargs currently for numpy functions, func: {func}" + ), f"**kwargs are currently not supported for numpy functions, func: {func}" return tracing_func(*args, **kwargs) def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index d87b510ae..50083fbf5 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -8,7 +8,7 @@ from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_d from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.values import EncryptedScalar -from hdk.hnumpy.tracing import trace_numpy_function +from hdk.numpy.tracing import trace_numpy_function @pytest.mark.parametrize( diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 0c992f329..78f7d4e49 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -6,7 +6,7 @@ from pathlib import Path from hdk.common.compilation import CompilationArtifacts from hdk.common.data_types.integers import UnsignedInteger from hdk.common.values import EncryptedScalar -from hdk.hnumpy.compile import compile_numpy_function +from hdk.numpy.compile import compile_numpy_function def test_artifacts_export(): diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index f6668a816..aa37d3d38 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -8,7 +8,7 @@ import pytest from hdk.common.compilation import CompilationConfiguration from hdk.common.data_types.integers import Integer from hdk.common.values import EncryptedScalar -from hdk.hnumpy.compile import compile_numpy_function_into_op_graph +from hdk.numpy.compile import compile_numpy_function_into_op_graph def no_fuse(x): diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py index 03fae0069..8dd3ffebc 100644 --- a/tests/common/debugging/test_drawing.py +++ b/tests/common/debugging/test_drawing.py @@ -6,7 +6,7 @@ from pathlib import Path from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph from hdk.common.values import EncryptedScalar -from hdk.hnumpy.compile import compile_numpy_function_into_op_graph +from hdk.numpy.compile import compile_numpy_function_into_op_graph def test_draw_graph_with_saving(): diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 740e8e382..1e9d51f0f 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -10,7 +10,7 @@ from hdk.common.data_types.integers import Integer from hdk.common.extensions.table import LookupTable from hdk.common.representation import intermediate as ir from hdk.common.values import EncryptedScalar -from hdk.hnumpy import tracing +from hdk.numpy import tracing def test_lookup_table_size_constraints(): diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 65884812d..c0981992b 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -11,7 +11,7 @@ from hdk.common.data_types.integers import Integer from hdk.common.extensions.table import LookupTable from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from hdk.common.values import ClearScalar, EncryptedScalar -from hdk.hnumpy.compile import compile_numpy_function_into_op_graph +from hdk.numpy.compile import compile_numpy_function_into_op_graph def add(x, y): diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 2ceb70774..e4e8b8b3f 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -8,7 +8,7 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.optimization.topological import fuse_float_operations from hdk.common.values import EncryptedScalar -from hdk.hnumpy.tracing import trace_numpy_function +from hdk.numpy.tracing import trace_numpy_function def no_fuse(x): diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index 3ab3e5d4e..81e0c2c4e 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -8,7 +8,7 @@ from hdk.common import check_op_graph_is_integer_program, is_a_power_of_2 from hdk.common.data_types.floats import Float64 from hdk.common.data_types.integers import Integer from hdk.common.values import EncryptedScalar -from hdk.hnumpy.tracing import trace_numpy_function +from hdk.numpy.tracing import trace_numpy_function @pytest.mark.parametrize( diff --git a/tests/hnumpy/test_compile.py b/tests/numpy/test_compile.py similarity index 99% rename from tests/hnumpy/test_compile.py rename to tests/numpy/test_compile.py index 7c2fa862b..7cf6dff53 100644 --- a/tests/hnumpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -1,4 +1,4 @@ -"""Test file for hnumpy compilation functions""" +"""Test file for numpy compilation functions""" import itertools import random @@ -10,7 +10,7 @@ from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable from hdk.common.values import EncryptedScalar, EncryptedTensor -from hdk.hnumpy.compile import ( +from hdk.numpy.compile import ( compile_numpy_function, compile_numpy_function_into_op_graph, ) diff --git a/tests/hnumpy/test_debugging.py b/tests/numpy/test_debugging.py similarity index 92% rename from tests/hnumpy/test_debugging.py rename to tests/numpy/test_debugging.py index 720878588..d6099a42d 100644 --- a/tests/hnumpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -1,4 +1,4 @@ -"""Test file for hnumpy debugging functions""" +"""Test file for debugging functions""" import numpy import pytest @@ -7,7 +7,7 @@ from hdk.common.data_types.integers import Integer from hdk.common.debugging import draw_graph, get_printable_graph from hdk.common.extensions.table import LookupTable from hdk.common.values import ClearScalar, EncryptedScalar, EncryptedTensor -from hdk.hnumpy import tracing +from hdk.numpy import tracing LOOKUP_TABLE_FROM_2B_TO_4B = LookupTable([9, 2, 4, 11]) LOOKUP_TABLE_FROM_3B_TO_2B = LookupTable([0, 1, 3, 2, 2, 3, 1, 0]) @@ -133,8 +133,8 @@ def issue_130_c(x, y): ), ], ) -def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): - "Test hnumpy get_printable_graph and draw_graph" +def test_print_and_draw_graph(lambda_f, ref_graph_str, x_y): + "Test get_printable_graph and draw_graph" x, y = x_y graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) @@ -164,8 +164,8 @@ def test_hnumpy_print_and_draw_graph(lambda_f, ref_graph_str, x_y): ), ], ) -def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph_str): - "Test hnumpy get_printable_graph and draw_graph on graphs with direct table lookup" +def test_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph_str): + "Test get_printable_graph and draw_graph on graphs with direct table lookup" graph = tracing.trace_numpy_function(lambda_f, params) draw_graph(graph, show=False) @@ -194,8 +194,8 @@ def test_hnumpy_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph # pylint: enable=unnecessary-lambda ], ) -def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): - "Test hnumpy get_printable_graph and draw_graph on graphs with dot" +def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): + "Test get_printable_graph and draw_graph on graphs with dot" graph = tracing.trace_numpy_function(lambda_f, params) draw_graph(graph, show=False) @@ -239,8 +239,8 @@ def test_hnumpy_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): ), ], ) -def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): - """Test hnumpy get_printable_graph with show_data_types""" +def test_print_with_show_data_types(lambda_f, x_y, ref_graph_str): + """Test get_printable_graph with show_data_types""" x, y = x_y graph = tracing.trace_numpy_function(lambda_f, {"x": x, "y": y}) @@ -284,8 +284,8 @@ def test_hnumpy_print_with_show_data_types(lambda_f, x_y, ref_graph_str): ), ], ) -def test_hnumpy_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref_graph_str): - """Test hnumpy get_printable_graph with show_data_types on graphs with direct table lookup""" +def test_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref_graph_str): + """Test get_printable_graph with show_data_types on graphs with direct table lookup""" graph = tracing.trace_numpy_function(lambda_f, params) draw_graph(graph, show=False) diff --git a/tests/hnumpy/test_np_dtypes_helpers.py b/tests/numpy/test_np_dtypes_helpers.py similarity index 96% rename from tests/hnumpy/test_np_dtypes_helpers.py rename to tests/numpy/test_np_dtypes_helpers.py index e1433e19c..bf97541b5 100644 --- a/tests/hnumpy/test_np_dtypes_helpers.py +++ b/tests/numpy/test_np_dtypes_helpers.py @@ -1,11 +1,11 @@ -"""Test file for hnumpy numpy dtype helpers""" +"""Test file for numpy dtype helpers""" import numpy import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.hnumpy.np_dtypes_helpers import ( +from hdk.numpy.np_dtypes_helpers import ( convert_base_data_type_to_numpy_dtype, convert_numpy_dtype_to_base_data_type, ) diff --git a/tests/hnumpy/test_tracing.py b/tests/numpy/test_tracing.py similarity index 96% rename from tests/hnumpy/test_tracing.py rename to tests/numpy/test_tracing.py index a16223260..fa03267d8 100644 --- a/tests/hnumpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -1,4 +1,4 @@ -"""Test file for hnumpy tracing""" +"""Test file for numpy tracing""" import networkx as nx import numpy @@ -8,7 +8,7 @@ from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer from hdk.common.representation import intermediate as ir from hdk.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor -from hdk.hnumpy import tracing +from hdk.numpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] @@ -53,8 +53,8 @@ OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] ), ], ) -def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): - "Test hnumpy tracing a binary operation (in the supported ops)" +def test_numpy_tracing_binary_op(operation, x, y, test_helpers): + "Test numpy tracing a binary operation (in the supported ops)" # Remark that the functions here have a common structure (which is # 2x op y), such that creating further the ref_graph is easy, by @@ -121,8 +121,8 @@ def test_hnumpy_tracing_binary_op(operation, x, y, test_helpers): ClearTensor, ], ) -def test_hnumpy_tracing_tensor_constant(tensor_constructor): - "Test hnumpy tracing tensor constant" +def test_numpy_tracing_tensor_constant(tensor_constructor): + "Test numpy tracing tensor constant" def simple_add_tensor(x): return x + numpy.array([[1, 2], [3, 4]], dtype=numpy.int32) @@ -261,7 +261,7 @@ def test_tracing_astype( ), ], ) -def test_trace_hnumpy_supported_ufuncs(inputs, expected_output_node, expected_output_value): +def test_trace_numpy_supported_ufuncs(inputs, expected_output_node, expected_output_value): """Function to trace supported numpy ufuncs""" for function_to_trace_def in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: @@ -339,7 +339,7 @@ def test_trace_hnumpy_ufuncs_not_supported(): # pylint: enable=unnecessary-lambda ], ) -def test_trace_hnumpy_dot(function_to_trace, inputs, expected_output_node, expected_output_value): +def test_trace_numpy_dot(function_to_trace, inputs, expected_output_node, expected_output_value): """Function to test dot tracing""" op_graph = tracing.trace_numpy_function(function_to_trace, inputs) From 9ffe9b667ac9e44f2ed3f2a9120d8c6c74ba80b6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 2 Sep 2021 14:07:30 +0200 Subject: [PATCH 0176/1104] refacto: fix missed renaming after bad merge --- hdk/numpy/tracing.py | 2 +- tests/numpy/test_tracing.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hdk/numpy/tracing.py b/hdk/numpy/tracing.py index d63b13b16..d5247dc38 100644 --- a/hdk/numpy/tracing.py +++ b/hdk/numpy/tracing.py @@ -42,7 +42,7 @@ class NPTracer(BaseTracer): tracing_func = self.get_tracing_func_for_np_function(ufunc) assert ( len(kwargs) == 0 - ), f"hnumpy does not support **kwargs currently for numpy ufuncs, ufunc: {ufunc}" + ), f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc}" return tracing_func(*input_tracers, **kwargs) raise NotImplementedError("Only __call__ method is supported currently") diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index fa03267d8..521c1bec6 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -280,7 +280,7 @@ def test_trace_numpy_supported_ufuncs(inputs, expected_output_node, expected_out assert op_graph.output_nodes[0].outputs[0] == expected_output_value -def test_trace_hnumpy_ufuncs_not_supported(): +def test_trace_numpy_ufuncs_not_supported(): """Testing a failure case of trace_numpy_function""" inputs = {"x": EncryptedScalar(Integer(128, is_signed=True))} From cfe48cca15ddbfa0a344e25f7f6a0cd37334c106 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 2 Sep 2021 10:55:13 +0200 Subject: [PATCH 0177/1104] test: add correctness tests for all supported ufunc's closes #263 --- pyproject.toml | 8 +++ .../common/optimization/test_float_fusing.py | 69 ++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5d89778af..310462a89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,3 +33,11 @@ myst-parser = "^0.15.1" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +filterwarnings = [ + "error", + "ignore::UserWarning", +] + + diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index e4e8b8b3f..65c06a751 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -8,6 +8,7 @@ import pytest from hdk.common.data_types.integers import Integer from hdk.common.optimization.topological import fuse_float_operations from hdk.common.values import EncryptedScalar +from hdk.numpy import tracing from hdk.numpy.tracing import trace_numpy_function @@ -36,7 +37,7 @@ def simple_fuse_output(x): return x.astype(numpy.float64).astype(numpy.int32) -def complex_fuse_indirect_input(x, y): +def complex_fuse_indirect_input(function, x, y): """Complex fuse""" intermediate = x + y intermediate = intermediate + 2 @@ -44,7 +45,7 @@ def complex_fuse_indirect_input(x, y): intermediate = intermediate.astype(numpy.int32) x_p_1 = intermediate + 1.5 x_p_2 = intermediate + 2.7 - x_p_3 = numpy.rint(x_p_1 + x_p_2) + x_p_3 = function(x_p_1 + x_p_2) return ( x_p_3.astype(numpy.int32), x_p_2.astype(numpy.int32), @@ -55,11 +56,11 @@ def complex_fuse_indirect_input(x, y): ) -def complex_fuse_direct_input(x, y): +def complex_fuse_direct_input(function, x, y): """Complex fuse""" - x_p_1 = x + 1.5 - x_p_2 = x + 2.7 - x_p_3 = numpy.rint(x_p_1 + x_p_2) + x_p_1 = x + 0.1 + x_p_2 = x + 0.2 + x_p_3 = function(x_p_1 + x_p_2) return ( x_p_3.astype(numpy.int32), x_p_2.astype(numpy.int32), @@ -77,8 +78,16 @@ def complex_fuse_direct_input(x, y): pytest.param(no_fuse_unhandled, False, id="no_fuse_unhandled"), pytest.param(simple_fuse_not_output, True, id="no_fuse"), pytest.param(simple_fuse_output, True, id="no_fuse"), - pytest.param(complex_fuse_indirect_input, True, id="complex_fuse_indirect_input"), - pytest.param(complex_fuse_direct_input, True, id="complex_fuse_direct_input"), + pytest.param( + lambda x, y: complex_fuse_indirect_input(numpy.rint, x, y), + True, + id="complex_fuse_indirect_input_with_rint", + ), + pytest.param( + lambda x, y: complex_fuse_direct_input(numpy.rint, x, y), + True, + id="complex_fuse_direct_input_with_rint", + ), ], ) @pytest.mark.parametrize("input_", [0, 2, 42, 44]) @@ -105,3 +114,47 @@ def test_fuse_float_operations(function_to_trace, fused, input_): num_params = len(params_names) inputs = (input_,) * num_params assert function_to_trace(*inputs) == op_graph(*inputs) + + +def test_fuse_float_operations_correctness(): + """Test functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC + with fuse_float_operations.""" + + for fun in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: + + if fun == numpy.arccosh: + input_list = [1, 2, 42, 44] + super_fun_list = [complex_fuse_direct_input] + elif fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: + input_list = [0, 0.1, 0.2] + super_fun_list = [complex_fuse_direct_input] + else: + input_list = [0, 2, 42, 44] + super_fun_list = [complex_fuse_direct_input, complex_fuse_indirect_input] + + for super_fun in super_fun_list: + + for input_ in input_list: + + def get_function_to_trace(): + return lambda x, y: super_fun(fun, x, y) + + function_to_trace = get_function_to_trace() + + params_names = signature(function_to_trace).parameters.keys() + + op_graph = trace_numpy_function( + function_to_trace, + {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + ) + orig_num_nodes = len(op_graph.graph) + fuse_float_operations(op_graph) + fused_num_nodes = len(op_graph.graph) + + assert fused_num_nodes < orig_num_nodes + + input_ = numpy.int32(input_) + + num_params = len(params_names) + inputs = (input_,) * num_params + assert function_to_trace(*inputs) == op_graph(*inputs) From f686ca535af53cac3c6b02bb03e992943c7f4139 Mon Sep 17 00:00:00 2001 From: youben11 Date: Wed, 1 Sep 2021 14:18:32 +0100 Subject: [PATCH 0178/1104] feat(mlir): convert TensorValue inputs to MLIR factorized the input type conversion of scalar values --- hdk/common/data_types/dtypes_helpers.py | 62 ++++++++++++++++++ hdk/common/mlir/mlir_converter.py | 82 +++++++++++++++++++++--- tests/common/mlir/test_mlir_converter.py | 55 +++++++++++++++- 3 files changed, 189 insertions(+), 10 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 7502c3a29..7c2c027b3 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -84,6 +84,68 @@ def value_is_scalar_integer(value_to_check: BaseValue) -> bool: ) +def value_is_encrypted_tensor_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is an encrypted TensorValue of type Integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is an encrypted TensorValue of type Integer + """ + return ( + isinstance(value_to_check, TensorValue) + and value_to_check.is_encrypted + and isinstance(value_to_check.data_type, INTEGER_TYPES) + ) + + +def value_is_encrypted_tensor_unsigned_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is an encrypted TensorValue of type unsigned Integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is an encrypted TensorValue of type Integer and + unsigned + """ + return ( + value_is_encrypted_tensor_integer(value_to_check) + and not cast(Integer, value_to_check.data_type).is_signed + ) + + +def value_is_clear_tensor_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is a clear TensorValue of type Integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is a clear TensorValue of type Integer + """ + return ( + isinstance(value_to_check, TensorValue) + and value_to_check.is_clear + and isinstance(value_to_check.data_type, INTEGER_TYPES) + ) + + +def value_is_tensor_integer(value_to_check: BaseValue) -> bool: + """Helper function to check that a value is a TensorValue of type Integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is a TensorValue of type Integer + """ + return isinstance(value_to_check, TensorValue) and isinstance( + value_to_check.data_type, INTEGER_TYPES + ) + + def find_type_to_hold_both_lossy( dtype1: BaseDataType, dtype2: BaseDataType, diff --git a/hdk/common/mlir/mlir_converter.py b/hdk/common/mlir/mlir_converter.py index 1e0124251..b3657cd9f 100644 --- a/hdk/common/mlir/mlir_converter.py +++ b/hdk/common/mlir/mlir_converter.py @@ -1,19 +1,29 @@ """File containing code to convert a DAG containing ir nodes to the compiler opset.""" # pylint: disable=no-name-in-module,no-member -from typing import cast +from typing import Tuple, cast import networkx as nx import zamalang from mlir.dialects import builtin -from mlir.ir import Context, InsertionPoint, IntegerType, Location, Module +from mlir.ir import ( + Context, + InsertionPoint, + IntegerType, + Location, + Module, + RankedTensorType, +) from mlir.ir import Type as MLIRType +from mlir.ir import UnrankedTensorType from zamalang.dialects import hlfhe from .. import values from ..data_types import Integer from ..data_types.dtypes_helpers import ( value_is_clear_scalar_integer, + value_is_clear_tensor_integer, value_is_encrypted_scalar_unsigned_integer, + value_is_encrypted_tensor_unsigned_integer, ) from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -41,6 +51,52 @@ class MLIRConverter: self.context = Context() zamalang.register_dialects(self.context) + def _get_tensor_element_type( + self, + bit_width: int, + is_encrypted: bool, + is_signed: bool, + shape: Tuple[int, ...], + ) -> MLIRType: + """Get the MLIRType for a tensor element given its properties. + + Args: + bit_width (int): number of bits used for the scalar + is_encrypted (bool): is the scalar encrypted or not + is_signed (bool): is the scalar signed or not + shape (Tuple[int, ...]): shape of the tensor + + Returns: + MLIRType: corresponding MLIR type + """ + element_type = self._get_scalar_element_type(bit_width, is_encrypted, is_signed) + if len(shape): # randked tensor + return RankedTensorType.get(shape, element_type) + # unranked tensor + return UnrankedTensorType.get(element_type) + + def _get_scalar_element_type( + self, bit_width: int, is_encrypted: bool, is_signed: bool + ) -> MLIRType: + """Get the MLIRType for a scalar element given its properties. + + Args: + bit_width (int): number of bits used for the scalar + is_encrypted (bool): is the scalar encrypted or not + is_signed (bool): is the scalar signed or not + + Returns: + MLIRType: corresponding MLIR type + """ + if is_encrypted and not is_signed: + return hlfhe.EncryptedIntegerType.get(self.context, bit_width) + if is_signed and not is_encrypted: # clear signed + return IntegerType.get_signed(bit_width) + # shoulld be clear unsigned at this point + assert not is_signed and not is_encrypted + # unsigned integer are considered signless in the compiler + return IntegerType.get_signless(bit_width) + def hdk_value_to_mlir_type(self, value: values.BaseValue) -> MLIRType: """Convert an HDK value to its corresponding MLIR Type. @@ -51,15 +107,25 @@ class MLIRConverter: corresponding MLIR type """ if value_is_encrypted_scalar_unsigned_integer(value): - return hlfhe.EncryptedIntegerType.get( - self.context, cast(Integer, value.data_type).bit_width + return self._get_scalar_element_type( + cast(Integer, value.data_type).bit_width, True, False ) if value_is_clear_scalar_integer(value): dtype = cast(Integer, value.data_type) - if dtype.is_signed: - return IntegerType.get_signed(dtype.bit_width, context=self.context) - # unsigned integer are considered signless in the compiler - return IntegerType.get_signless(dtype.bit_width, context=self.context) + return self._get_scalar_element_type(dtype.bit_width, False, dtype.is_signed) + if value_is_encrypted_tensor_unsigned_integer(value): + dtype = cast(Integer, value.data_type) + return self._get_tensor_element_type( + dtype.bit_width, True, False, cast(values.TensorValue, value).shape + ) + if value_is_clear_tensor_integer(value): + dtype = cast(Integer, value.data_type) + return self._get_tensor_element_type( + dtype.bit_width, + False, + dtype.is_signed, + cast(values.TensorValue, value).shape, + ) raise TypeError(f"can't convert value of type {type(value)} to MLIR type") def convert(self, op_graph: OPGraph) -> str: diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index c0981992b..de975ea22 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -3,7 +3,7 @@ import itertools import pytest -from mlir.ir import IntegerType +from mlir.ir import IntegerType, Location, RankedTensorType, UnrankedTensorType from zamalang import compiler from zamalang.dialects import hlfhe @@ -11,6 +11,7 @@ from hdk.common.data_types.integers import Integer from hdk.common.extensions.table import LookupTable from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from hdk.common.values import ClearScalar, EncryptedScalar +from hdk.common.values.tensors import ClearTensor, EncryptedTensor from hdk.numpy.compile import compile_numpy_function_into_op_graph @@ -202,14 +203,64 @@ def test_hdk_clear_integer_to_mlir_type(is_signed): """Test conversion of ClearScalar into MLIR""" value = ClearScalar(Integer(5, is_signed=is_signed)) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) - int_mlir = converter.hdk_value_to_mlir_type(value) with converter.context: + int_mlir = converter.hdk_value_to_mlir_type(value) if is_signed: assert int_mlir == IntegerType.get_signed(5) else: assert int_mlir == IntegerType.get_signless(5) +@pytest.mark.parametrize("is_signed", [True, False]) +@pytest.mark.parametrize( + "shape", + [ + None, + (5,), + (5, 8), + (-1, 5), + ], +) +def test_hdk_clear_tensor_integer_to_mlir_type(is_signed, shape): + """Test conversion of ClearTensor into MLIR""" + value = ClearTensor(Integer(5, is_signed=is_signed), shape) + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + with converter.context, Location.unknown(): + tensor_mlir = converter.hdk_value_to_mlir_type(value) + if is_signed: + element_type = IntegerType.get_signed(5) + else: + element_type = IntegerType.get_signless(5) + if shape is None: + expected_type = UnrankedTensorType.get(element_type) + else: + expected_type = RankedTensorType.get(shape, element_type) + assert tensor_mlir == expected_type + + +@pytest.mark.parametrize( + "shape", + [ + None, + (5,), + (5, 8), + (-1, 5), + ], +) +def test_hdk_encrypted_tensor_integer_to_mlir_type(shape): + """Test conversion of EncryptedTensor into MLIR""" + value = EncryptedTensor(Integer(6, is_signed=False), shape) + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + with converter.context, Location.unknown(): + tensor_mlir = converter.hdk_value_to_mlir_type(value) + element_type = hlfhe.EncryptedIntegerType.get(converter.context, 6) + if shape is None: + expected_type = UnrankedTensorType.get(element_type) + else: + expected_type = RankedTensorType.get(shape, element_type) + assert tensor_mlir == expected_type + + def test_failing_hdk_to_mlir_type(): """Test failing conversion of an unsupported type into MLIR""" value = "random" From 31c1787af2b40fa65dce0e65ffbf2c8711c80507 Mon Sep 17 00:00:00 2001 From: youben11 Date: Wed, 1 Sep 2021 15:00:54 +0100 Subject: [PATCH 0179/1104] feat(mlir): conversion of dot node into MLIR --- hdk/common/mlir/converters.py | 34 +++++++++++++++++++++++- hdk/common/mlir/mlir_converter.py | 29 +++++++++++--------- hdk/common/mlir/utils.py | 14 +++++++--- tests/common/mlir/test_converters.py | 4 +-- tests/common/mlir/test_mlir_converter.py | 22 +++++++++++++++ 5 files changed, 84 insertions(+), 19 deletions(-) diff --git a/hdk/common/mlir/converters.py b/hdk/common/mlir/converters.py index df615689f..a457253da 100644 --- a/hdk/common/mlir/converters.py +++ b/hdk/common/mlir/converters.py @@ -17,7 +17,9 @@ from zamalang.dialects import hlfhe from ...common.data_types.integers import Integer from ..data_types.dtypes_helpers import ( value_is_clear_scalar_integer, + value_is_clear_tensor_integer, value_is_encrypted_scalar_unsigned_integer, + value_is_encrypted_tensor_integer, ) from ..representation import intermediate as ir @@ -131,7 +133,7 @@ def constant(node, _, __, ctx): def apply_lut(node, preds, ir_to_mlir_node, ctx): - """Converted function for the arbitrary function intermediate node.""" + """Converter function for the arbitrary function intermediate node.""" assert len(node.inputs) == 1, "LUT should have a single input" assert len(node.outputs) == 1, "LUT should have a single output" if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): @@ -156,12 +158,42 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): ).result +def dot(node, preds, ir_to_mlir_node, ctx): + """Converter function for the dot intermediate node.""" + assert len(node.inputs) == 2, "Dot should have two inputs" + assert len(node.outputs) == 1, "Dot should have a single output" + if not ( + ( + value_is_encrypted_tensor_integer(node.inputs[0]) + and value_is_clear_tensor_integer(node.inputs[1]) + ) + or ( + value_is_encrypted_tensor_integer(node.inputs[1]) + and value_is_clear_tensor_integer(node.inputs[0]) + ) + ): + raise TypeError( + f"Don't support subtraction between {type(node.inputs[0])} and {type(node.inputs[1])}" + ) + lhs_node, rhs_node = preds + # need to flip as underlying operation need encrypted first + if value_is_clear_tensor_integer(node.inputs[0]): + lhs_node, rhs_node = rhs_node, lhs_node + lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] + return hlfhe.Dot( + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + lhs, + rhs, + ).result + + V0_OPSET_CONVERSION_FUNCTIONS = { ir.Add: add, ir.Sub: sub, ir.Mul: mul, ir.Constant: constant, ir.ArbitraryFunction: apply_lut, + ir.Dot: dot, } # pylint: enable=no-name-in-module,no-member diff --git a/hdk/common/mlir/mlir_converter.py b/hdk/common/mlir/mlir_converter.py index b3657cd9f..7c50c0988 100644 --- a/hdk/common/mlir/mlir_converter.py +++ b/hdk/common/mlir/mlir_converter.py @@ -51,7 +51,7 @@ class MLIRConverter: self.context = Context() zamalang.register_dialects(self.context) - def _get_tensor_element_type( + def _get_tensor_type( self, bit_width: int, is_encrypted: bool, @@ -69,13 +69,13 @@ class MLIRConverter: Returns: MLIRType: corresponding MLIR type """ - element_type = self._get_scalar_element_type(bit_width, is_encrypted, is_signed) + element_type = self._get_scalar_integer_type(bit_width, is_encrypted, is_signed) if len(shape): # randked tensor return RankedTensorType.get(shape, element_type) # unranked tensor return UnrankedTensorType.get(element_type) - def _get_scalar_element_type( + def _get_scalar_integer_type( self, bit_width: int, is_encrypted: bool, is_signed: bool ) -> MLIRType: """Get the MLIRType for a scalar element given its properties. @@ -92,7 +92,7 @@ class MLIRConverter: return hlfhe.EncryptedIntegerType.get(self.context, bit_width) if is_signed and not is_encrypted: # clear signed return IntegerType.get_signed(bit_width) - # shoulld be clear unsigned at this point + # should be clear unsigned at this point assert not is_signed and not is_encrypted # unsigned integer are considered signless in the compiler return IntegerType.get_signless(bit_width) @@ -107,24 +107,29 @@ class MLIRConverter: corresponding MLIR type """ if value_is_encrypted_scalar_unsigned_integer(value): - return self._get_scalar_element_type( + return self._get_scalar_integer_type( cast(Integer, value.data_type).bit_width, True, False ) if value_is_clear_scalar_integer(value): dtype = cast(Integer, value.data_type) - return self._get_scalar_element_type(dtype.bit_width, False, dtype.is_signed) + return self._get_scalar_integer_type( + dtype.bit_width, is_encrypted=False, is_signed=dtype.is_signed + ) if value_is_encrypted_tensor_unsigned_integer(value): dtype = cast(Integer, value.data_type) - return self._get_tensor_element_type( - dtype.bit_width, True, False, cast(values.TensorValue, value).shape + return self._get_tensor_type( + dtype.bit_width, + is_encrypted=True, + is_signed=False, + shape=cast(values.TensorValue, value).shape, ) if value_is_clear_tensor_integer(value): dtype = cast(Integer, value.data_type) - return self._get_tensor_element_type( + return self._get_tensor_type( dtype.bit_width, - False, - dtype.is_signed, - cast(values.TensorValue, value).shape, + is_encrypted=False, + is_signed=dtype.is_signed, + shape=cast(values.TensorValue, value).shape, ) raise TypeError(f"can't convert value of type {type(value)} to MLIR type") diff --git a/hdk/common/mlir/utils.py b/hdk/common/mlir/utils.py index 77b374708..c6dec7c8d 100644 --- a/hdk/common/mlir/utils.py +++ b/hdk/common/mlir/utils.py @@ -4,7 +4,9 @@ from typing import cast from ..data_types import Integer from ..data_types.dtypes_helpers import ( value_is_clear_scalar_integer, + value_is_clear_tensor_integer, value_is_encrypted_scalar_integer, + value_is_encrypted_tensor_integer, value_is_scalar_integer, ) from ..operator_graph import OPGraph @@ -37,9 +39,11 @@ def _set_all_bit_width(op_graph: OPGraph, p: int): """ for node in op_graph.graph.nodes: for value in node.outputs + node.inputs: - if value_is_clear_scalar_integer(value): + if value_is_clear_scalar_integer(value) or value_is_clear_tensor_integer(value): value.data_type.bit_width = p + 1 - elif value_is_encrypted_scalar_integer(value): + elif value_is_encrypted_scalar_integer(value) or value_is_encrypted_tensor_integer( + value + ): value.data_type.bit_width = p @@ -52,8 +56,10 @@ def update_bit_width_for_mlir(op_graph: OPGraph): max_bit_width = 0 for node in op_graph.graph.nodes: for value_out in node.outputs: - if value_is_clear_scalar_integer(value_out): + if value_is_clear_scalar_integer(value_out) or value_is_clear_tensor_integer(value_out): max_bit_width = max(max_bit_width, value_out.data_type.bit_width - 1) - elif value_is_encrypted_scalar_integer(value_out): + elif value_is_encrypted_scalar_integer(value_out) or value_is_encrypted_tensor_integer( + value_out + ): max_bit_width = max(max_bit_width, value_out.data_type.bit_width) _set_all_bit_width(op_graph, max_bit_width) diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index ffcf3aff5..04ce27223 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -3,7 +3,7 @@ import pytest from hdk.common.data_types.floats import Float from hdk.common.data_types.integers import Integer -from hdk.common.mlir.converters import add, apply_lut, constant, mul, sub +from hdk.common.mlir.converters import add, apply_lut, constant, dot, mul, sub from hdk.common.values import ClearScalar, EncryptedScalar @@ -21,7 +21,7 @@ class MockNode: self.outputs = outputs -@pytest.mark.parametrize("converter", [add, sub, mul]) +@pytest.mark.parametrize("converter", [add, sub, mul, dot]) def test_failing_converter(converter): """Test failing converter""" with pytest.raises(TypeError, match=r"Don't support .* between .* and .*"): diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index de975ea22..1b45ce0d7 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -2,6 +2,7 @@ # pylint: disable=no-name-in-module,no-member import itertools +import numpy import pytest from mlir.ir import IntegerType, Location, RankedTensorType, UnrankedTensorType from zamalang import compiler @@ -66,6 +67,11 @@ def lut(x): return table[x] +def dot(x, y): + """Test dot""" + return numpy.dot(x, y) + + def datagen(*args): """Generate data from ranges""" for prod in itertools.product(*args): @@ -178,6 +184,22 @@ def datagen(*args): }, (range(0, 8),), ), + ( + dot, + { + "x": EncryptedTensor(Integer(64, is_signed=False), shape=(4,)), + "y": ClearTensor(Integer(64, is_signed=False), shape=(4,)), + }, + (range(0, 8), range(0, 8)), + ), + ( + dot, + { + "x": ClearTensor(Integer(64, is_signed=False), shape=(4,)), + "y": EncryptedTensor(Integer(64, is_signed=False), shape=(4,)), + }, + (range(0, 8), range(0, 8)), + ), ], ) def test_mlir_converter(func, args_dict, args_ranges): From 2e459a344cf6c85a6bc8a6978745022e70fee493 Mon Sep 17 00:00:00 2001 From: youben11 Date: Fri, 3 Sep 2021 07:49:09 +0100 Subject: [PATCH 0180/1104] refactor(dtype_helpers): homogenize functions --- hdk/common/data_types/dtypes_helpers.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/hdk/common/data_types/dtypes_helpers.py b/hdk/common/data_types/dtypes_helpers.py index 7c2c027b3..0879521ee 100644 --- a/hdk/common/data_types/dtypes_helpers.py +++ b/hdk/common/data_types/dtypes_helpers.py @@ -31,11 +31,7 @@ def value_is_encrypted_scalar_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is an encrypted ScalarValue of type Integer """ - return ( - isinstance(value_to_check, ScalarValue) - and value_to_check.is_encrypted - and isinstance(value_to_check.data_type, INTEGER_TYPES) - ) + return value_is_scalar_integer(value_to_check) and value_to_check.is_encrypted def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> bool: @@ -63,11 +59,7 @@ def value_is_clear_scalar_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is a clear ScalarValue of type Integer """ - return ( - isinstance(value_to_check, ScalarValue) - and value_to_check.is_clear - and isinstance(value_to_check.data_type, INTEGER_TYPES) - ) + return value_is_scalar_integer(value_to_check) and value_to_check.is_clear def value_is_scalar_integer(value_to_check: BaseValue) -> bool: @@ -93,11 +85,7 @@ def value_is_encrypted_tensor_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is an encrypted TensorValue of type Integer """ - return ( - isinstance(value_to_check, TensorValue) - and value_to_check.is_encrypted - and isinstance(value_to_check.data_type, INTEGER_TYPES) - ) + return value_is_tensor_integer(value_to_check) and value_to_check.is_encrypted def value_is_encrypted_tensor_unsigned_integer(value_to_check: BaseValue) -> bool: @@ -125,11 +113,7 @@ def value_is_clear_tensor_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is a clear TensorValue of type Integer """ - return ( - isinstance(value_to_check, TensorValue) - and value_to_check.is_clear - and isinstance(value_to_check.data_type, INTEGER_TYPES) - ) + return value_is_tensor_integer(value_to_check) and value_to_check.is_clear def value_is_tensor_integer(value_to_check: BaseValue) -> bool: From 0fde0ae83612ee8b5ad7cc6527b1d554d36314a7 Mon Sep 17 00:00:00 2001 From: youben11 Date: Fri, 3 Sep 2021 08:18:21 +0100 Subject: [PATCH 0181/1104] fix(tests): wasn't expecting bits to be update to max --- tests/numpy/test_compile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 7cf6dff53..33db2540b 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -213,8 +213,8 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): (4,), # Remark that, when you do the dot of tensors of 4 values between 0 and 3, # you can get a maximal value of 4*3*3 = 36, ie something on 6 bits - "%0 = x # Integer" - "\n%1 = y # Integer" + "%0 = x # Integer" + "\n%1 = y # Integer" "\n%2 = Dot(0, 1) # Integer" "\nreturn(%2)\n", ), From b71664e38f38971234b3c3e622bcfe2f3f7320b2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 2 Sep 2021 14:32:41 +0200 Subject: [PATCH 0182/1104] tools: ignore imports for code similarity in pylint --- pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index 3160c4a70..65bbb6a0b 100644 --- a/pylintrc +++ b/pylintrc @@ -384,7 +384,7 @@ ignore-comments=yes ignore-docstrings=yes # Ignore imports when computing similarities. -ignore-imports=no +ignore-imports=yes # Ignore function signatures when computing similarities. ignore-signatures=no From 468686a92fb8bb2dbabc514f57b2769af26d3228 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 2 Sep 2021 15:28:34 +0200 Subject: [PATCH 0183/1104] tools: add pytest_nb target to easily test notebooks --- .github/workflows/continuous-integration.yaml | 2 +- Makefile | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index b2e21364e..fe97021cd 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -82,7 +82,7 @@ jobs: run: | make strip_nb make notebook_timeout - poetry run pytest --nbmake examples/*.ipynb + make pytest_nb - name: Test coverage id: coverage if: ${{ steps.pytest.outcome != 'skipped' && !cancelled() }} diff --git a/Makefile b/Makefile index d68796f11..7f300a877 100644 --- a/Makefile +++ b/Makefile @@ -161,6 +161,10 @@ notebook_timeout: poetry run python ./script/nbmake_utils/notebook_test_timeout.py examples .PHONY: notebook_timeout +pytest_nb: + poetry run pytest --nbmake examples/*.ipynb +.PHONY: pytest_nb + benchmark: poetry run pytest benchmarks/ --benchmark-save=findings .PHONY: benchmark From 97ce55447f3c1c5beaf0c8aa3360b826bac9fc79 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 2 Sep 2021 16:03:21 +0200 Subject: [PATCH 0184/1104] refactor: modify the imports to have everything in numpy - allows users to do import hdk.numpy as hnp and use hnp for everything - update notebooks and docs --- benchmarks/test_compilation_and_evaluation.py | 22 +- docs/dev/COMPILATION.md | 10 +- examples/QuantizedLinearRegression.ipynb | 444 ++++++++-------- examples/QuantizedLogisticRegression.ipynb | 488 ++++++++---------- hdk/common/data_types/__init__.py | 3 +- hdk/common/values/__init__.py | 1 + hdk/numpy/__init__.py | 23 +- 7 files changed, 468 insertions(+), 523 deletions(-) diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py index 6e9b9496d..5949fcf47 100644 --- a/benchmarks/test_compilation_and_evaluation.py +++ b/benchmarks/test_compilation_and_evaluation.py @@ -4,9 +4,7 @@ import itertools import pytest -from hdk.common.data_types.integers import SignedInteger, UnsignedInteger -from hdk.common.values import EncryptedScalar -from hdk.numpy.compile import compile_numpy_function_into_op_graph +import hdk.numpy as hnp @pytest.mark.parametrize( @@ -14,13 +12,16 @@ from hdk.numpy.compile import compile_numpy_function_into_op_graph [ pytest.param( lambda x: x + 42, - {"x": EncryptedScalar(SignedInteger(4))}, + {"x": hnp.EncryptedScalar(hnp.SignedInteger(4))}, ((-2, 2),), id="x + 42", ), pytest.param( lambda x, y: x + y, - {"x": EncryptedScalar(SignedInteger(4)), "y": EncryptedScalar(UnsignedInteger(4))}, + { + "x": hnp.EncryptedScalar(hnp.SignedInteger(4)), + "y": hnp.EncryptedScalar(hnp.UnsignedInteger(4)), + }, ((-2, 2), (20, 30)), id="x + y", ), @@ -35,7 +36,7 @@ def test_compilation(benchmark, function, parameters, ranges): @benchmark def compilation(): - compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) + hnp.compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) @pytest.mark.parametrize( @@ -43,7 +44,7 @@ def test_compilation(benchmark, function, parameters, ranges): [ pytest.param( lambda x: x + 420, - {"x": EncryptedScalar(SignedInteger(4))}, + {"x": hnp.EncryptedScalar(hnp.SignedInteger(4))}, ((-2, 2),), [ {0: -2}, @@ -54,7 +55,10 @@ def test_compilation(benchmark, function, parameters, ranges): ), pytest.param( lambda x, y: x + y, - {"x": EncryptedScalar(SignedInteger(4)), "y": EncryptedScalar(UnsignedInteger(4))}, + { + "x": hnp.EncryptedScalar(hnp.SignedInteger(4)), + "y": hnp.EncryptedScalar(hnp.UnsignedInteger(4)), + }, ((-2, 2), (20, 30)), [ {0: -2, 1: 25}, @@ -72,7 +76,7 @@ def test_evaluation(benchmark, function, parameters, ranges, inputs): for prod in itertools.product(*args): yield prod - graph = compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) + graph = hnp.compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) @benchmark def evaluation(): diff --git a/docs/dev/COMPILATION.md b/docs/dev/COMPILATION.md index 876eefaf6..bc6bb55e7 100644 --- a/docs/dev/COMPILATION.md +++ b/docs/dev/COMPILATION.md @@ -11,20 +11,18 @@ However, one can already build interesting and impressing use cases, and more wi ```python # Import necessary HDK components -from hdk.common.data_types.integers import UnsignedInteger -from hdk.common.values import EncryptedScalar, EncryptedTensor -from hdk.numpy.compile import compile_numpy_function +import hdk.numpy as hnp # Define the function to homomorphize def f(x, y): return (2 * x) + y # Define the inputs of homomorphized function -x = EncryptedScalar(UnsignedInteger(2)) -y = EncryptedScalar(UnsignedInteger(1)) +x = hnp.EncryptedScalar(hnp.UnsignedInteger(2)) +y = hnp.EncryptedScalar(hnp.UnsignedInteger(1)) # Compile the function to its homomorphic equivalent -engine = compile_numpy_function( +engine = hnp.compile_numpy_function( f, {"x": x, "y": y}, iter([(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)]), ) diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 5ecdaaedd..4a5e42d5c 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -2,127 +2,113 @@ "cells": [ { "cell_type": "markdown", - "id": "0fe629d6", - "metadata": {}, "source": [ "# Quantized Linear Regression\n", "\n", "Currently, **hdk** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "d0cfb561", - "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our linear regression model" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 1, - "id": "3c1d929c", - "metadata": {}, - "outputs": [], "source": [ "import numpy as np" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "69d25f7c", - "metadata": {}, "source": [ "### And some helpers for visualization" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 2, - "id": "a89c1a6c", - "metadata": {}, - "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "7729c1de", - "metadata": {}, "source": [ "### We need a dataset, a handcrafted one for simplicity" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 3, - "id": "b77a9e82", - "metadata": {}, - "outputs": [], "source": [ "x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", "y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "cc8673ff", - "metadata": {}, "source": [ "### Let's visualize our dataset to get a grasp of it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 4, - "id": "35a98d1a", - "metadata": {}, - "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 5, - "id": "56703410", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "

" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "e31b82e8", - "metadata": {}, "source": [ "### Now, we need a model so let's define it\n", "\n", "The main purpose of this tutorial is not to train a linear regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 6, - "id": "cc5e72a2", - "metadata": {}, - "outputs": [], "source": [ "class Model:\n", " w = None\n", @@ -144,160 +130,145 @@ "\n", " def evaluate(self, x):\n", " return x @ self.w + self.b" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "cefd8346", - "metadata": {}, "source": [ "### And create one" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 7, - "id": "b9879f4d", - "metadata": {}, - "outputs": [], "source": [ "model = Model().fit(x, y)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "01cfc83f", - "metadata": {}, "source": [ "### Time to make some predictions" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 8, - "id": "78356d37", - "metadata": {}, - "outputs": [], "source": [ "inputs = np.linspace(40, 210, 100).reshape(-1, 1)\n", "predictions = model.evaluate(inputs)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "58160140", - "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 9, - "id": "2a623999", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "ax.plot(inputs, predictions, color=\"blue\")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "d3f39faa", - "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 10, - "id": "7fa65211", - "metadata": {}, + "source": [ + "print(model.w)\n", + "print(model.b)" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "[[2.669915]]\n", "-3.2335143\n" ] } ], - "source": [ - "print(model.w)\n", - "print(model.b)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "544d6e34", - "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "abf310f2", - "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 11, - "id": "a7b3b993", - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "from IPython.display import SVG\n", "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ] + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + }, + "metadata": {}, + "execution_count": 11 + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "9cbd7e1d", - "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 12, - "id": "a8bab855", - "metadata": {}, - "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -430,65 +401,59 @@ " domain = np.array(range(2**input_bits), dtype=np.uint)\n", " table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", " return QuantizedFunction(table)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "e5be0800", - "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 13, - "id": "3ec0ad9b", - "metadata": {}, - "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(model.w, parameter_bits)\n", "b_q = QuantizedArray.of(model.b, parameter_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "b43c0371", - "metadata": {}, "source": [ "### And quantize our inputs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 14, - "id": "20cea447", - "metadata": {}, - "outputs": [], "source": [ "input_bits = 6\n", "\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "ca76b68d", - "metadata": {}, "source": [ "### Time to make quantized inference" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 15, - "id": "8728e939", - "metadata": {}, - "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -497,52 +462,48 @@ "y_q = x_q.affine(w_q, b_q, min_y, max_y, output_bits)\n", "\n", "quantized_predictions = y_q.dequantize()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "ab782b4a", - "metadata": {}, "source": [ "### And visualize the results" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 16, - "id": "9d2bb5da", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "ax.plot(inputs, quantized_predictions, color=\"black\")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "4834cdfc", - "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 17, - "id": "fcf4ea26", - "metadata": {}, - "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (max_y - min_y)\n", "zp_y = int(round(min_y * q_y))\n", @@ -558,12 +519,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "43e47369", - "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -580,14 +541,28 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ] + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "### Let's import the hdk numpy package now!" + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import hdk.numpy as hnp" + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 18, - "id": "2de0cf20", - "metadata": {}, - "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -597,62 +572,57 @@ "f = lambda intermediate: (c1 * (intermediate + c3)) - c4\n", "f_q = QuantizedFunction.of(f, input_bits + parameter_bits, output_bits)\n", "\n", - "from hdk.common.extensions.table import LookupTable\n", - "table = LookupTable([int(entry) for entry in f_q.table])\n", + "table = hnp.LookupTable([int(entry) for entry in f_q.table])\n", "\n", "w_0 = int(c2.flatten()[0])\n", "\n", "def infer(x_0):\n", " return table[(x_0 + zp_x) * w_0]" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "93eb9499", - "metadata": {}, "source": [ "### Time to compile our quantized inference function" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 19, - "id": "a80895fd", - "metadata": {}, - "outputs": [], "source": [ - "from hdk.common.data_types.integers import Integer\n", - "from hdk.common.values import EncryptedScalar\n", - "from hdk.numpy.compile import compile_numpy_function_into_op_graph\n", - "\n", "dataset = []\n", "for x_i in x_q:\n", " dataset.append((int(x_i[0]),))\n", "\n", - "homomorphic_model = compile_numpy_function_into_op_graph(\n", + "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", " infer,\n", - " {\"x_0\": EncryptedScalar(Integer(input_bits, is_signed=False))},\n", + " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", " iter(dataset),\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "f0b08a0f", - "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 20, - "id": "2cc4e11d", - "metadata": {}, + "source": [ + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "\n", "%0 = Constant(1) # Integer\n", @@ -665,49 +635,40 @@ ] } ], - "source": [ - "from hdk.common.debugging import get_printable_graph\n", - "print(get_printable_graph(homomorphic_model, show_data_types=True))" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 21, - "id": "785c50ce", - "metadata": {}, + "source": [ + "hnp.draw_graph(homomorphic_model).show()" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", "text/plain": [ "" - ] + ], + "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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" }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "from hdk.common.debugging import draw_graph\n", - "draw_graph(homomorphic_model).show()" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "ade14f17", - "metadata": {}, "source": [ "### Finally, it's time to make homomorphic inference\n", "\n", "Or, at least, simulate it until the compiler integration is complete." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 22, - "id": "dd2d03d7", - "metadata": {}, - "outputs": [], "source": [ "homomorphic_predictions = []\n", "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", @@ -715,45 +676,44 @@ " inference = QuantizedArray(evaluation[homomorphic_model.output_nodes[0]], y_q.parameters)\n", " homomorphic_predictions.append(inference.dequantize())\n", "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "443fbc03", - "metadata": {}, "source": [ "### And visualize it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 23, - "id": "57050b5d", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "53ecca94", - "metadata": {}, "source": [ "### Enjoy!" - ] + ], + "metadata": {} } ], "metadata": {}, diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 3da58515c..6fe14dd2b 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -2,107 +2,84 @@ "cells": [ { "cell_type": "markdown", - "id": "0fe629d6", - "metadata": {}, "source": [ "# Quantized Logistic Regression\n", "\n", "Currently, **hdk** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "d0cfb561", - "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our logistic regression model" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 1, - "id": "3c1d929c", - "metadata": {}, - "outputs": [], "source": [ "import numpy as np\n", "import torch" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "69d25f7c", - "metadata": {}, "source": [ "### And some helpers for visualization" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 2, - "id": "a89c1a6c", - "metadata": {}, - "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "7729c1de", - "metadata": {}, "source": [ "### We need a dataset, a handcrafted one for simplicity" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 3, - "id": "b77a9e82", - "metadata": {}, - "outputs": [], "source": [ "x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n", "y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "cc8673ff", - "metadata": {}, "source": [ "### Let's visualize our dataset to get a grasp of it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 4, - "id": "35a98d1a", - "metadata": {}, - "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 5, - "id": "56703410", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "x_min, x_max = x[:, 0].min(), x[:, 0].max()\n", "x_deviation = x_max - x_min\n", @@ -126,22 +103,31 @@ " color=\"blue\",\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "e31b82e8", - "metadata": {}, "source": [ "### Now, we need a model so let's define it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 6, - "id": "cc5e72a2", - "metadata": {}, - "outputs": [], "source": [ "class Model(torch.nn.Module):\n", " def __init__(self, n):\n", @@ -151,27 +137,45 @@ " def forward(self, x):\n", " output = torch.sigmoid(self.fc(x))\n", " return output" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "cefd8346", - "metadata": {}, "source": [ "### And create one\n", "\n", "The main purpose of this tutorial is not to train a logistic regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 7, - "id": "b9879f4d", - "metadata": {}, + "source": [ + "model = Model(x.shape[1])\n", + "\n", + "optimizer = torch.optim.SGD(model.parameters(), lr=1)\n", + "criterion = torch.nn.BCELoss()\n", + "\n", + "epochs = 1501\n", + "for e in range(1, epochs + 1):\n", + " optimizer.zero_grad()\n", + "\n", + " out = model(x)\n", + " loss = criterion(out, y)\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " if e % 100 == 1 or e == epochs:\n", + " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "Epoch: 1 | Loss: 0.9475528597831726\n", "Epoch: 101 | Loss: 0.13412582874298096\n", @@ -192,40 +196,18 @@ ] } ], - "source": [ - "model = Model(x.shape[1])\n", - "\n", - "optimizer = torch.optim.SGD(model.parameters(), lr=1)\n", - "criterion = torch.nn.BCELoss()\n", - "\n", - "epochs = 1501\n", - "for e in range(1, epochs + 1):\n", - " optimizer.zero_grad()\n", - "\n", - " out = model(x)\n", - " loss = criterion(out, y)\n", - "\n", - " loss.backward()\n", - " optimizer.step()\n", - "\n", - " if e % 100 == 1 or e == epochs:\n", - " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "01cfc83f", - "metadata": {}, "source": [ "### Time to make some predictions" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 8, - "id": "78356d37", - "metadata": {}, - "outputs": [], "source": [ "contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 250)\n", "contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 250)\n", @@ -233,33 +215,20 @@ "\n", "inputs = np.stack((contour_plot_x_data.flatten(), contour_plot_y_data.flatten()), axis=1)\n", "predictions = model(torch.tensor(inputs).float()).detach().numpy()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "58160140", - "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 9, - "id": "2a623999", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "contour = ax.contourf(\n", " contour_plot_x_data,\n", @@ -269,25 +238,42 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "d3f39faa", - "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 10, - "id": "7fa65211", - "metadata": {}, + "source": [ + "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", + "b = model.fc.bias.flatten().tolist()[0]\n", + "\n", + "print(w)\n", + "print(b)" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "[[4.53723335]\n", " [2.37176466]]\n", @@ -295,71 +281,56 @@ ] } ], - "source": [ - "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", - "b = model.fc.bias.flatten().tolist()[0]\n", - "\n", - "print(w)\n", - "print(b)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "544d6e34", - "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "abf310f2", - "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 11, - "id": "d3ab2aa2", - "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "from IPython.display import SVG\n", "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ] + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + }, + "metadata": {}, + "execution_count": 11 + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "e4314038", - "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 12, - "id": "a8bab855", - "metadata": {}, - "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -513,66 +484,60 @@ " def apply(self, x):\n", " assert x.parameters == self.input_parameters\n", " return QuantizedArray(self.table[x.values], self.output_parameters)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "e5be0800", - "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 13, - "id": "3ec0ad9b", - "metadata": {}, - "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(w, parameter_bits)\n", "b_q = QuantizedArray.of(b, parameter_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "b43c0371", - "metadata": {}, "source": [ "### And quantize our inputs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 14, - "id": "20cea447", - "metadata": {}, - "outputs": [], "source": [ "input_bits = 5\n", "\n", "x = inputs\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "ca76b68d", - "metadata": {}, "source": [ "### Time to make quantized inference" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 15, - "id": "8728e939", - "metadata": {}, - "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -583,33 +548,20 @@ "y_q = sigmoid.apply(intermediate_q)\n", "\n", "quantized_predictions = y_q.dequantize()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "ab782b4a", - "metadata": {}, "source": [ "### And visualize the results" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 16, - "id": "9d2bb5da", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -622,22 +574,31 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "4834cdfc", - "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 17, - "id": "fcf4ea26", - "metadata": {}, - "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (intermediate.max() - intermediate.min())\n", "zp_y = int(round(intermediate.min() * q_y))\n", @@ -653,12 +614,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "43e47369", - "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -675,14 +636,28 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ] + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "### Let's import the hdk numpy package now!" + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import hdk.numpy as hnp" + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 18, - "id": "2de0cf20", - "metadata": {}, - "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -700,66 +675,61 @@ "\n", "f_q = QuantizedFunction.of(f, output_bits, output_bits)\n", "\n", - "from hdk.common.extensions.table import LookupTable\n", - "table = LookupTable([int(entry) for entry in f_q.table])\n", + "table = hnp.LookupTable([int(entry) for entry in f_q.table])\n", "\n", "w_0 = int(c2.flatten()[0])\n", "w_1 = int(c2.flatten()[1])\n", "\n", "def infer(x_0, x_1):\n", " return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)]" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "93eb9499", - "metadata": {}, "source": [ "### Time to compile our quantized inference function" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 19, - "id": "a80895fd", - "metadata": {}, - "outputs": [], "source": [ - "from hdk.common.data_types.integers import Integer\n", - "from hdk.common.values import EncryptedScalar\n", - "from hdk.numpy.compile import compile_numpy_function_into_op_graph\n", - "\n", "dataset = []\n", "for x_i in x_q:\n", " dataset.append((int(x_i[0]), int(x_i[1])))\n", " \n", - "homomorphic_model = compile_numpy_function_into_op_graph(\n", + "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", " infer,\n", " {\n", - " \"x_0\": EncryptedScalar(Integer(input_bits, is_signed=False)),\n", - " \"x_1\": EncryptedScalar(Integer(input_bits, is_signed=False)),\n", + " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", + " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " },\n", " iter(dataset),\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "f0b08a0f", - "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 20, - "id": "2cc4e11d", - "metadata": {}, + "source": [ + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "\n", "%0 = Constant(2) # Integer\n", @@ -778,49 +748,40 @@ ] } ], - "source": [ - "from hdk.common.debugging import get_printable_graph\n", - "print(get_printable_graph(homomorphic_model, show_data_types=True))" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 21, - "id": "9d1ff32f", - "metadata": {}, + "source": [ + "hnp.draw_graph(homomorphic_model).show()" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbsAAAIbCAYAAABynTBtAAB/gUlEQVR4nO3dd3hU1dbA4d+khw5KDShFAtIEqSJNwYJIEaVKRwQEQcWrKHqxYVe8gtKl+CkdQRFULr1IkaI0L12KFCG0AOnr+2NnMglJIGVmzpT1+uTJmcKcNdu9s07ZxSYiglJKKeW75gZYHYFSSinlaprslFJK+TxNdkoppXxekJU7P3kSdu0yP0eOwIkT5rnTp+HCBUhKgsuXISEB8uSB0FAIC4NChaBkSYiIgFKlIDISqleHKlUgb14rv5F3OQnsSv45ApxIfu40cAFIAi4DCUAeIBQIAwoBJYEIoBQQCVQHqgBa/P5D26+1tP1mj81dHVQSE2HHDli92vysXw/nzjl3HzabaTiNG0OTJtCsGZQp49x9eKtEYAewOvlnPeDk4seGaTiNgSZAM0CL3zdo+7WWtt9cm+vSZBcXB8uXw3ffwaJFcObMjd8fEADFi0OxYlCkCAQGQv78EBQEV69CbCxcuwZRUeYo8tKlm8dw113Qvj089pg5evQnccBy4DtgEXCT4icAKA4UA4oAgUB+zOn/VSAWuAZEYY4is1D83AW0Bx7DHD0q76Ht11rafp3KNcnuwAGYOBGmToWzZ9O/HhhoKnHt2lCtGlStCpUqQYkSpmFk1dWrcPQo7NkDu3ebyykbNsDx4xm/v1YtGDAAunaFfPly9t28wQFgIjAVyKD4CcRU4tpANaAqUAkoQfaua18FjgJ7gN2YyykbgEyKn1rAAKAr4MPF7/W0/VpL269LODfZbdwIb78NS5fC9Z9arhy0awfNm0OjRlCwoLP2mt7Bg7BmjYlj6VKIjk77eoEC0L8/vPiiOQr1FRuBt4GlwPX/U8sB7YDmQCPAhcXPQWBNchxLgeuKnwJAf+BFzFGo8gzafq2l7del5iJO8NtvIg8/LGKaiOOnRAmR4cNFtm1zxl5y5to1kUWLRDp2FAkJSRtf3rwiL74oEhVlXXzO8JuIPCwiXPdTQkSGi4iFxS/XRGSRiHQUkRBJG19eEXlRRLy8+L2etl9raft1izm5Snbnz4sMGiQSGJi2Et57r8icOSJxcU4K00lOnRJ5912RkiXTxlusmMi0aSJJSVZHmD3nRWSQiARK2kp4r4jMEREPK345JSLvikhJSRtvMRGZJiJeVvxeT9uvtc6Ltl83ynmy+/FHkeLF01a6Jk1Eli93ZnyucfWqyGefpW80zZqJHDtmdXRZ86OIFJe0la6JiHhB8ctVEflM0jeaZiLiJcXv9bT9Wkvbr9tlP9nFxYmMHCkSEOCoZKVKiUyf7oLwXOzKFfNdQkMd36VgQXNU66niRGSkiASIo5KVEhEvLH65Iua7hIrjuxQUc1SrXEPbr7W0/Vome8kuKkqkcWNHxbLZRIYOFYmOdlF4brJnj0j9+mm/1zvvWB1VelEi0lgcFcsmIkNFxMuLX/aISH1J+708sPi9nrZfa2n7tVTWk93RoyJVq6a9Tr5kiStjc6+4OJGXX057xDtwoEhCgtWRGUdFpKqkvU7uQ8UvcSLysqQ94h0oIh5S/F5P26+1tP1aLmvJ7uRJkQoVHJWoenXvuTaeXd99JxIe7viu/fpZf+P7pIhUEEclqi4efW08V74TkXBxfNd+4vE3vj2etl9rY9L26xFunuwuXBCpWdNReZo2Nb24fNm6dSJFiji+84gR1sVyQURqiqPyNBXTi8uXrRORIuL4zhYWv9fT9qvt1908tP3ePNm1beuoNA0aeP/1/azauNGM47F/91mzrImjrTgqTQPx/uv7WbVRzDge+3e3qPi9nrZfbb9W8MD2O+eGS/yMG2fmxAOoXBkWL/afWcnr14d58xzTH/Xvb2Z2d6dxmDnxACoDi/HtWclTqw/MwzH9UX/MzO4q67T9avu1iie230yT3ZEjMGyY2Q4Lgzlz4JZb3BSVh3j4YXj9dbN98SL07eu+fR8BkoufMGAO4GfFz8NAcvFzEXBj8Xs9bb/afq3mae0302Q3YoSZoRzg/fc9d8bxHTt20KpVKwoVKkT+/Plp0aIF69evd9rnjxgB995rtlesMEfH7jACM0M5wPu4f8bx+Ph4Ro8eTe3atcmfPz/FihWjZcuW/PDDD4i4ZVUowJRDcvGzAnN0rG7OW9ovwJIlS4iMjCQoO7NIZ5G2X22/KTK6uLltmxmrAiLVqnlO993rbdy4UcLDw6VTp07y999/yz///CP9+vWToKAg+fnnn522n23bHF2aq1Z1fe+ubWLGqiAi1cT93Xejo6OlUaNGUqNGDVm9erVcvXpV/vrrL3niiScEkJ07d7o1nm3i6NJcVTymd5fH8pb2e+DAAWndurXUqFFDChQoIIGBgS7Zj7Zfbb+SWQeVfv0cN3YXL3Z3TFmTmJgoVatWlZIlS8rVq1dTnk9ISJBKlSpJmTJlJCYmxmn7e/JJR5msXOm0j81QP3Hc2LWi+AcOHCgFChSQU6dOpXk+OjpaQkND3d5YRESeFEeZrHT73r2LN7RfEZEuXbrIe++9J/Hx8RIREeGyZCei7VfE79tv+mR39apIoUKmUpQvb/0YlcysXLlSAHn22WfTvfbGG28IIPPmzXPa/jZtcjSW7t2d9rHpXBWRQmIqRXlx/1HQqVOnJDAwUAYOHOjmPd/YJnE0FhcWv9fzlvYrImkOUl2d7LT9WssD2m/63pjLlsGFC2a7b1+w2dx4TTUbVqxYAUCdOnXSvWZ/bvny5U7bX716jvseCxdCYqLTPjqNZcCF5O2+gLuL//vvvycxMZFGjRq5ec83Vg/HfY+FgIuK3+t5S/sFCA8Pd9u+tP1ayxPab7pkt26dY/uRR5yzk0aNGmGz2VJ+unXrBkCLFi3SPH/B3kqz4M8//wSgdOnS6V6LiIgAYN++fbkPPhV7eVy+DH/84dSPTpGq+HFS8WfLtm3bAChcuDDDhg2jTJkyhISEcPvttzNkyBCioqIsiMqwl8dlwEXF7/W8pf1aQduvf7ffdMlu0ybzO18+5/XgWrduHTt27CBv3rzcddddTJgwAYAff/yR+vXrM3PmTESEQoUKZfkz7Q0rbwYDh/LlM4vGnz9/Ptexp9awoWN740anfnSK5OInH+7vwQVw8uRJAPr06cPp06dZvXo1Z86c4e233+arr77innvu4eLFixZEBqmKHxcVv9fzlvZrBW2//t1+0yW748fN78hICAx03o7uuusupk6dyu+//06PHj0QEfr370/z5s3p3Lmz83YEKV1rbU6+hlOlimPbXk7OZv/YSMCJxZ9lMTExgLnENG3aNMqXL0+hQoXo0aMHr7zyCvv27eOTTz6xIDJIVfy4qPi9ni+0X1fR9uvf7Tddsjt71vwuWtT5O+vQoQMjRoxgwYIFNGrUiHPnzvH222/n6LPsR5FXrlxJ95r9OWcfaaYelGsvJ2ezf6wLij9L7GfKLVq0SDfuqXXr1gD8/PPPbo8L0g7KdVHxez1vab9W0Pbr3+03XbKzD0R11b3jt99+m/r167NhwwY6dOhAQMANZyzLVOXKlQE4nsEh2okTJwCIjIzMeaAZSH3FNIMc6xT2gajuu3WfVtmyZQG4JYPpNooVKwbAP//8486QUqS+YO2i4vd63tJ+raDt17/bb7qaWriw+e3k210pVq1axcWLF6levTrPPPMMv//+e44+57777gNg69at6V6zP9e8efOcB5qBc+cc266aeim5+HFR8d+UvReX/dp/amfOnAGgePHibo3JLlXx+93US1nlLe3XCtp+/bv9pkt29kpw+rTzd3b48GH69u3L/Pnz+f777wkPD6dt27Y5OtJo2rQpVapUYd68eSnXqQESExOZNWsWZcqUoVWrVs4Mn+S6AkCRIk796BT2SuCC4s+SRx55hIiICH766ac05Qrwww8/ANCuXTsLIoNUxY+Lit/reUv7tYK2Xz9vv9ePvGvXzgy8DAgwa2E5y+XLl6VGjRqyaNGilOdWrVolwcHB0qRJE4mLi8v2Z/76668SFhYmnTt3lpMnT8rZs2elf//+EhQUJD/99JPzgk82aZJjYOrs2U7/eBERaSdm4GWAmLWwrLB06VIJCgqStm3byr59++T8+fMyY8YMyZs3r9SvXz/NYGB3miSOgakuKn6v503tNzVXDyoX0fbr5+03/QwqH33kqBDOml5y0KBBAqT87Ny5U/755580zwHy9ttvZ/uzt23bJi1btpQCBQpIvnz55P7775d169Y5J/Dr9O7tKJujR12yC/lIHBXCebN7Zt+GDRvkoYcekoIFC0pISIhUrlxZ3njjDcsaiohIb3GUjYuK3+t5U/v94Ycf0n2G/WfSpEnOCT4Vbb9+3X7n2ETSToG9ebNZCwrMDAyTJ7v63NI7xMRARARERUG5cnDokGv2sxmzFhSYGRi0+I0YIAKIAsoBLip+r6ftN2Pafq3lAe13brp7dnXrmjE6ALNnQ3S0u2PyTAsXmoYC0L276/ZTFzNGB2A2oMVvLMQ0FAAXFr/X0/abMW2/1lqI9e03XbKz2aBXL7MdHQ3/+Y+bI/JASUnw4Ydm22aDnj1dty8b0Ct5OxrQ4ockILn4sQEuLH6vp+03PW2/1vKU9pvhIJn+/R1dmD/8MG0vJldLPddeZj9vvPGG+wICZsyA7dvNdqdOUL68a/fXH0cX5g9J24vJH80AkoufToCLi9/raftNS9uvtTym/WZ2Ny/1je727d13F9HTnDolUry4KYeQEJEDB9yz39Q3uv24+OWUiBQXUw4hIuKm4vd62n4Nbb/W8qD2m36JH7vBgyF5khIWLIBJk9yVfj1HUhI8+aRjzNLzz0OFCu7Z92AgufhZAPhh8ZMEPIljzNLzgJuK3+tp+9X2azWPa783SoV//CESFuY4KnJWV2Zv8dxzjqPj2rVFYmPdu/8/RCRMHEdFflb88pw4jo5ri4ibi9/rafvV9mslD2u/mZ/ZgVkixH5jNy4OOnSADGbn8klvvQWffWa2CxeGWbMgJMS9MVTHcWM3DugA+Enx8xbwWfJ2YWAW4Obi93rafs22tl/388j2m5WUOGyY4wgpXz4RF0xO4jGSkkRGjnR83/BwkTVrrI1pmDiOkPKJiA8XvySJyEhxfN9wEbG4+L2etl9rY9L26xHSz6CSkaQkkZ49HRUoNFRk+nQXh2aBK1dEunRxfM+QEJFUsyNZJklEeoqjAoWKiA8Wv1wRkS7i+J4hIuIBxe/1tP1aS9uvR8hashNJf8QEIt27i1y+7MLw3GjPHpHq1dMeAS9danVUDtcfMSEi3UXER4pf9ohIdUl7BOxBxe/1tP1aS9uv5bKe7OxGjxYJCnJUqooVRX75xQWhuUlMjMioUeZyh/07lSkjsn271ZFlbLSIBImjUlUUES8ufokRkVFiLnfYv1MZEdluYUy+TNuvtUaLtl+LZD/ZiYisXy9y++1pjxI7dBA5fNi50bnajz+KREam/R5t24qcO2d1ZDe2XkRul7RHiR1E5LB1IeXIjyISKWm/R1sR8fDi93rafq2l7dcSOUt2IiJRUSI9eojYbI6KFhws8tRTIocOOTNG51u6VKRBg7SNpFAhkXHjzOUebxAlIj1ExCaOihYsIk+JiIcXvywVkQaStpEUEpFxYi73KNfT9mstbb9ul/NkZ7dmTdpr5fa1tFq0EJkzRyQhwRlx5t7FiyITJojUrJk2VvtR7alTVkeYM2sk7bVyxKyl1UJE5oiIhxS/XBSRCSJSU9LGaj+q9dLi93rafq2l7ddtcp/sRETi40WmTRO54470FbFsWZF//Utk40b3H3VFR4vMnSvSubNI3rxp47LZRFq3FvntN/fG5ArxIjJNRO6Q9BWxrIj8S0Q2ivuPuqJFZK6IdBaRvNfFZROR1iLiA8Xv9bT9Wkvbr1ukX88uNxISYOZMGDvWrKt1vYgIuP9+aNIEGjeGSpWctWcjNha2bIHVq2HtWlizBq5dS/ue0FB4/HF44QWoXdu5+7daAjATGItZV+t6EcD9QBOgMeDk4icW2AKsBtYCa4Drip9Q4HHgBcDHit/rafu1lrZfl5rr1GSX2vbtMHEizJsHZ89m/J4CBaBKFTPTQ2QklCwJZcpA8eLmtbAwyJvXzHwQHQ3x8XDpkvk5dszMeffXX7BnD+zaBfv3mwZrJAEHgYoAVKsGPXpA795w662u+MaeZTswEZgHZFL8FACqYGZ6iARKAmWA4smvhQF5MTMfRAPxwKXkn2OYOe/+AvYAu4D9mAabkWpAD6A34AfF7/Wsb79pafsF9u1zLFaItt9scl2ys0tMNEdqCxbAzz/DgQOu3JtDYOBHBAR8xPDhe+nW7ZbUdcSvJGKO1BYAPwNuKn6CMQtZtgba41jQUnkXq9pvcLBZiLZ1a2jfHr9vvx/Nm8fPXbog//uf69cowifbr+uT3fX+/ts0ng0bzNHczp1w7lzuPjMgAMqWNUeYNWuaSyxVq16iTp0qPPDAA0ydOtUZofuEvzGNZwPmaG4nkFL8R47Af/9rlnIODc3yZwYAZTFHmDUxl1gaYI4qlW9xTvs9CCwDBgAZt98GDcxZoYKLFy9StWpVHn74Yd6aPDnz9ptDftJ+3Z/sMnLqFBw6BCdPwokT5vLG5cvmGv7Vq+Z3/vwQFAT58plLJBER5rJJRARUrJhxw1iwYAGPP/44v/zyCw888ID7v5iXOAUcAmZMnMjUF17ghQsXuBIURCxwFXMtPz8QBOTDXCKJwFw2icBcKPaxhqGyIbvt9+TJ75k1qx0zZ56jUqXCmbZfZTz99NN899137N27l1szuIZrb78ngROYy5OXQdtvWp6R7Fypffv2bN++nV27dpFXW9QN9evXj/3797Nq1SqrQ1E+7MyZMxQvXlwPQrNg7dq1NG3alG+//ZbOnTtbHY43m3vDJX58wRdffMGFCxd45513rA7F423atIn69etbHYbyccWKFeP2229nc0ZdPlWK2NhYBgwYwMMPP6yJzgl8PtmVLFmSUaNG8fHHH7N9+3arw/FYV65cYe/evdStW9fqUJQfqFevnia7m3j33Xc5cuQIX3zxhdWh+ASfT3YAAwYMoEGDBvTv35/ExESrw/FIv/32GwkJCdSrV8/qUJQfqFu3Lhs3brQ6DI/1v//9jw8++IBRo0ZRrlw5q8PxCX6R7AICApg8eTJ//PEHY8aMsTocj7R582ZKlCjBbbfdZnUoyg/Ur1+fM2fOcPToUatD8ThJSUk89dRT1KhRg2effdbqcHyGXyQ7gEqVKvHSSy8xYsQIDh8+bHU4HmfLli16Vqfcpk6dOgQFBemlzAyMHz+ejRs3MmHCBAIDA60Ox2f4TbIDGDFiBGXLlmXQoEFWh+JxNm3apMlOuU2ePHmoUqWKJrvrnDx5khEjRvDiiy9Sq1Ytq8PxKX6V7EJDQxk/fjw//fQTs2bNsjocj2G/nKTJTrmTdlJJb9CgQRQqVIjXXnvN6lB8jl8lO4DGjRvz1FNP8eyzz3I2s0n//MzGjRux2WzUqVPH6lCUH6lbt25KxygFixcv5rvvvmPixIk6JtgF/C7ZAXz00UeEhoYyfPhwq0PxCFu2bCEyMpLChQtbHYryI/Xr108Z8uLvLl26xMCBA+nZs6cOtHcRv0x2BQsWZPTo0Xz11VcsX77c6nAsp/frlBWqVatG3rx52bRpk9WhWG748OHExMTw0UcfWR2Kz/LLZAfQoUMH2rRpw8CBA7l2/aJZfkRE+O233zTZKbcLDAzk7rvvZsuWLVaHYqlNmzYxYcIEPvvsM4oWLWp1OD7Lb5MdwJdffsmZM2cYNWqU1aFYZt++fZw/f16TnbKEv3dSiYuLo2/fvjzwwAM8+eSTVofj0/w62ZUqVYq3336bDz74gB07dlgdjiU2b95MSEgId911l9WhKD9Ur149du7cyZUrV6wOxRLvv/8+hw8f5ssvv7Q6FJ/n18kOTFffevXq+e1UYlu2bKFmzZqEZmP9OqWcpV69eiQmJvrlvLX79u3jvffe46233qK8GxZk9Xd+n+wCAgKYMGECO3bs8MujK13pQFmpbNmyFC9e3O86qYgIAwcOpFKlSgwZMsTqcPyC3yc7ML3C/vWvf/HKK69w5MgRq8Nxm7i4OH7//Xdd6UBZqm7dun7XSWXSpEmsXr2aKVOmEBwcbHU4fkGTXbLXX3+d2267za+mEtuxYwexsbHaOUVZyt86qZw6dYrhw4fz/PPPU7t2bavD8Rua7JKFhoYybtw4li5dyty5c60Oxy02b95MwYIFqVixotWhKD9Wr149Dh8+zOnTp60OxS2effZZChQowMiRI60Oxa9oskuladOm9OnThyFDhnD+/Hmrw3E5+0oHAQFaDZR16tati81m84tLmUuWLGHevHl88cUX5MuXz+pw/Ir+lbvOJ598QkBAAC+//LLVobicdk5RnqBIkSLccccdPp/sLl++zIABA+jWrRutWrWyOhy/o8nuOgULFuTTTz9l8uTJrFixwupwXObixYvs379fO6coj+AP9+1effVVrl69yieffGJ1KH5Jk10GOnXqROvWrRk4cCAxMTFWh+MSmzdvJikpSZOd8gj2ZCciVofiEps3b2bcuHF8+umnFCtWzOpw/JImu0yMGTOGkydP8u677zr1c3fs2EGrVq0oVKgQ+fPnp0WLFqxfv96p+8iKzZs3c9ttt1GyZMksvX/JkiVERkYSFBTk4siUP6pXrx5RUVEcPHgwW//OU9rTjSQkJNC/f3+aNGlC9+7dM32ftjHX0mSXidtuuy1lKrHdu3c75TM3bdpEw4YNyZ8/P3v37uXw4cOUL1+eZs2a8csvvzhlH1ll75xyMwcPHqRNmza88sorftNbTrlfrVq1CA0Nzdbgck9qTzfywQcf8L///Y9JkyZhs9nSva5tzE1EZSoxMVHuueceqV+/viQmJub6s6pWrSolS5aUq1evpjyfkJAglSpVkjJlykhMTExuQ86yUqVKyYcffnjT93Xp0kXee+89iY+Pl4iICAkMDHRDdMof1alTR4YOHZql93pae8rMvn37JCwsTD744INM36NtzC3m6JndDdinEtu2bRvjx4/P1WetWbOG3bt388QTTxAeHp7yfGBgIF26dOHYsWMsXrw4tyFnydGjR/n777+zdGY3ZcoUhg8frpdWlMtlp5OKJ7WnzEjylGCRkZE8//zzmb5P25h7aLK7ierVqzNs2DBeeeUVjh8/nuPPsffsrFOnTrrX7M+5ayHZzZs3ExAQwN13333T96b+Q6KUK9WrV4/t27cTFxd30/d6UnvKzFdffcXKlSuZMGHCDacE0zbmHprssuCNN96gZMmSDBgwIMef8eeffwJQunTpdK9FREQAZhZ0d9iyZQtVq1Ylf/78btmfUllRr149YmJi+OOPP276Xk9qTxk5e/Ysw4cPZ+jQoTRo0MCyOJSDJrssCA0NZfz48SxZsoQFCxbk6DMuXLgAQN68edO9Zp9JwV2ztmzevFnnw1Qep3LlyhQqVChLlzI9qT1lZPDgweTJk4e33nrLshhUWprssqhZs2b07NmTQYMGpTQ0Z5HksUUZ9dRytqSkJLZt26bJTnkcm81G7dq1cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", "text/plain": [ "" - ] + ], + "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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" }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "from hdk.common.debugging import draw_graph\n", - "draw_graph(homomorphic_model).show()" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "ade14f17", - "metadata": {}, "source": [ "### Finally, it's time to make homomorphic inference\n", "\n", "Or, at least, simulate it until the compiler integration is complete." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 22, - "id": "dd2d03d7", - "metadata": {}, - "outputs": [], "source": [ "homomorphic_predictions = []\n", "for x_0, x_1 in map(lambda x_i: (int(x_i[0]), int(x_i[1])), x_q):\n", @@ -828,33 +789,20 @@ " inference = QuantizedArray(evaluation[homomorphic_model.output_nodes[0]], y_q.parameters)\n", " homomorphic_predictions.append(inference.dequantize())\n", "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "443fbc03", - "metadata": {}, "source": [ "### And visualize it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 23, - "id": "57050b5d", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -867,15 +815,27 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/png": "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" + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "53ecca94", - "metadata": {}, "source": [ "### Enjoy!" - ] + ], + "metadata": {} } ], "metadata": {}, diff --git a/hdk/common/data_types/__init__.py b/hdk/common/data_types/__init__.py index 758381e62..0dbe4cd17 100644 --- a/hdk/common/data_types/__init__.py +++ b/hdk/common/data_types/__init__.py @@ -1,3 +1,4 @@ """Module for data types code and data structures.""" -from . import dtypes_helpers, integers +from . import dtypes_helpers, floats, integers +from .floats import Float, Float32, Float64 from .integers import Integer, SignedInteger, UnsignedInteger diff --git a/hdk/common/values/__init__.py b/hdk/common/values/__init__.py index b8c18b684..34bc45927 100644 --- a/hdk/common/values/__init__.py +++ b/hdk/common/values/__init__.py @@ -1,5 +1,6 @@ """Module for value structures.""" +from . import scalars, tensors from .base import BaseValue from .scalars import ClearScalar, EncryptedScalar, ScalarValue from .tensors import ClearTensor, EncryptedTensor, TensorValue diff --git a/hdk/numpy/__init__.py b/hdk/numpy/__init__.py index e310fe9b1..97cfc0789 100644 --- a/hdk/numpy/__init__.py +++ b/hdk/numpy/__init__.py @@ -1,2 +1,23 @@ """Module for compiling numpy functions to homomorphic equivalents.""" -from . import tracing + +from ..common.compilation import CompilationArtifacts, CompilationConfiguration +from ..common.data_types import ( + Float, + Float32, + Float64, + Integer, + SignedInteger, + UnsignedInteger, +) +from ..common.debugging import draw_graph, get_printable_graph +from ..common.extensions.table import LookupTable +from ..common.values import ( + ClearScalar, + ClearTensor, + EncryptedScalar, + EncryptedTensor, + ScalarValue, + TensorValue, +) +from .compile import compile_numpy_function, compile_numpy_function_into_op_graph +from .tracing import trace_numpy_function From 3e7644d4d5f192bbb9eeb5d8ba9b8172987275b4 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Thu, 2 Sep 2021 15:18:42 +0200 Subject: [PATCH 0185/1104] chore(issues): update bug issue template add artifacts instructions close #129 --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0661904c6..f904babb5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,6 +30,8 @@ print("Minimal POC to reproduce the bug") ## Artifacts +Attach all generated artifacts here (generated in the `.artifacts` directory by default, see documentation for more detailed instructions). +
Logs or output

From 00228e2817d0bece5214664899e0b56b64969c7d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 3 Sep 2021 16:46:23 +0200 Subject: [PATCH 0186/1104] chore: add BSD3 License --- LICENSE | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..dcc91f668 --- /dev/null +++ b/LICENSE @@ -0,0 +1,42 @@ +BSD 3-Clause Clear License + +Copyright © 2021 ZAMA. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +3. Neither the name of ZAMA nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY’S PATENT RIGHTS ARE GRANTED BY THIS LICENSE*. +THIS SOFTWARE IS PROVIDED BY THE ZAMA AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +ZAMA OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*EXEMPTION: ZAMA grants to the user a non-exclusive, free and non-commercial license on all +patents filed in its name relating to the open-source code (the “Patents”) for the sole +purpose of development, research, prototyping and experimentation. It is agreed that ZAMA +will support the user and the community to use, develop and improve Patents and the open-source +code. As a counterpart, the user undertakes to collaborate and share information with ZAMA +regarding any use, developments or improvements, whether protectable or not, of the exploited +Patents and open-source code. In this context it is agreed that the user will declare to ZAMA, +in writing and within a reasonable time, his/her wish to file any intellectual property right, +including in particular any patent or copyright application, relating to the open-source code +and/or the Patents. All declarations shall occur before the application is filed. Further to +this declaration, ZAMA will continue to support the user in the development, research, +prototyping and experimentation and may grant him the right to file the intellectual property +right. From 6b6aa7ee4e6403ca4068417eb574576be90dd360 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 6 Sep 2021 10:05:57 +0200 Subject: [PATCH 0187/1104] doc: add info from Rand about external PR closes #289 --- docs/dev/GETTING-STARTED.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index c7862b9da..fffd75179 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -160,7 +160,13 @@ After you finish your work, you can leave the docker by using the `exit` command ## Contributing -Now, you have a working environment, and you know what is where in the project. You are ready to contribute! Well, not so fast let's go over some other important things that you need to be careful about. +Now, you have a working environment, and you know what is where in the project. + +There are two ways to contribute to HDK: +- you can open issues to report bugs, typos and suggest ideas +- you can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests, so please make sure to get in touch before you do! + +Let's go over some other important things that you need to be careful about. ### Creating a new branch @@ -206,6 +212,8 @@ To learn more about conventional commits, check [this](https://www.conventionalc ### Before creating pull request +We remind that only official contributors can send pull requests. To become such an official contributor, please email hello@zama.ai. + You should rebase on top of `main` branch before you create your pull request. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. ```shell From 150d33ba4898fdfd6025c8a290bad3f8fe488e67 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 3 Sep 2021 09:47:34 +0200 Subject: [PATCH 0188/1104] feat: make get_printable_graph give correct info for np.dot closes #204 --- hdk/common/debugging/printing.py | 8 +++++- hdk/common/values/base.py | 3 +- hdk/common/values/scalars.py | 4 +++ hdk/common/values/tensors.py | 4 +++ tests/numpy/test_compile.py | 9 ++++-- tests/numpy/test_debugging.py | 48 +++++++++++++++++++++----------- 6 files changed, 53 insertions(+), 23 deletions(-) diff --git a/hdk/common/debugging/printing.py b/hdk/common/debugging/printing.py index 1c721472a..8cce142b1 100644 --- a/hdk/common/debugging/printing.py +++ b/hdk/common/debugging/printing.py @@ -18,7 +18,7 @@ def output_data_type_to_string(node): str: a string representing the datatypes of the outputs of the node """ - return ", ".join([str(o.data_type) for o in node.outputs]) + return ", ".join([str(o) for o in node.outputs]) def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: @@ -43,6 +43,11 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: for node in nx.topological_sort(graph): + # This code doesn't work with more than a single output. For more outputs, + # we would need to change the way the destination are created: currently, + # they only are done by incrementing i + assert len(node.outputs) == 1 + if isinstance(node, ir.Input): what_to_print = node.input_name elif isinstance(node, ir.Constant): @@ -74,6 +79,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: # Then, just print the predecessors in the right order what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" + # This code doesn't work with more than a single output new_line = f"%{i} = {what_to_print}" # Manage datatypes diff --git a/hdk/common/values/base.py b/hdk/common/values/base.py index c4ffd16dd..39fd770ef 100644 --- a/hdk/common/values/base.py +++ b/hdk/common/values/base.py @@ -17,8 +17,7 @@ class BaseValue(ABC): self._is_encrypted = is_encrypted def __repr__(self) -> str: # pragma: no cover - encrypted_str = "Encrypted" if self._is_encrypted else "Clear" - return f"{encrypted_str}{self.__class__.__name__}<{self.data_type!r}>" + return str(self) @abstractmethod def __eq__(self, other: object) -> bool: diff --git a/hdk/common/values/scalars.py b/hdk/common/values/scalars.py index c408b1637..38a9a87e4 100644 --- a/hdk/common/values/scalars.py +++ b/hdk/common/values/scalars.py @@ -10,6 +10,10 @@ class ScalarValue(BaseValue): def __eq__(self, other: object) -> bool: return BaseValue.__eq__(self, other) + def __str__(self) -> str: # pragma: no cover + encrypted_str = "Encrypted" if self._is_encrypted else "Clear" + return f"{encrypted_str}Scalar<{self.data_type!r}>" + def make_clear_scalar(data_type: BaseDataType) -> ScalarValue: """Helper to create a clear ScalarValue. diff --git a/hdk/common/values/tensors.py b/hdk/common/values/tensors.py index 1f4b9effa..966f7c8e7 100644 --- a/hdk/common/values/tensors.py +++ b/hdk/common/values/tensors.py @@ -35,6 +35,10 @@ class TensorValue(BaseValue): and super().__eq__(other) ) + def __str__(self) -> str: + encrypted_str = "Encrypted" if self._is_encrypted else "Clear" + return f"{encrypted_str}Tensor<{str(self.data_type)}, shape={self.shape}>" + @property def shape(self) -> Tuple[int, ...]: """The TensorValue shape property. diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 33db2540b..447445f0c 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -213,9 +213,12 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): (4,), # Remark that, when you do the dot of tensors of 4 values between 0 and 3, # you can get a maximal value of 4*3*3 = 36, ie something on 6 bits - "%0 = x # Integer" - "\n%1 = y # Integer" - "\n%2 = Dot(0, 1) # Integer" + "%0 = x " + "# EncryptedTensor, shape=(4,)>" + "\n%1 = y " + "# EncryptedTensor, shape=(4,)>" + "\n%2 = Dot(0, 1) " + "# EncryptedScalar>" "\nreturn(%2)\n", ), # pylint: enable=unnecessary-lambda diff --git a/tests/numpy/test_debugging.py b/tests/numpy/test_debugging.py index d6099a42d..2b791c56e 100644 --- a/tests/numpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -221,9 +221,9 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): EncryptedScalar(Integer(64, is_signed=False)), EncryptedScalar(Integer(32, is_signed=True)), ), - "%0 = x # Integer" - "\n%1 = y # Integer" - "\n%2 = Add(0, 1) # Integer" + "%0 = x # EncryptedScalar>" + "\n%1 = y # EncryptedScalar>" + "\n%2 = Add(0, 1) # EncryptedScalar>" "\nreturn(%2)\n", ), ( @@ -232,9 +232,12 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): EncryptedScalar(Integer(17, is_signed=False)), EncryptedScalar(Integer(23, is_signed=False)), ), - "%0 = x # Integer" - "\n%1 = y # Integer" - "\n%2 = Mul(0, 1) # Integer" + "%0 = x " + "# EncryptedScalar>" + "\n%1 = y " + "# EncryptedScalar>" + "\n%2 = Mul(0, 1) " + "# EncryptedScalar>" "\nreturn(%2)\n", ), ], @@ -259,27 +262,38 @@ def test_print_with_show_data_types(lambda_f, x_y, ref_graph_str): ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x # Integer" - "\n%1 = TLU(0) # Integer" + "%0 = x " + "# EncryptedScalar>" + "\n%1 = TLU(0) " + "# EncryptedScalar>" "\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x # Integer" - "\n%1 = Constant(4) # Integer" - "\n%2 = Add(0, 1) # Integer" - "\n%3 = TLU(2) # Integer" + "%0 = x " + "# EncryptedScalar>" + "\n%1 = Constant(4) " + "# ClearScalar>" + "\n%2 = Add(0, 1) " + "# EncryptedScalar>" + "\n%3 = TLU(2) " + "# EncryptedScalar>" "\nreturn(%3)\n", ), ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[LOOKUP_TABLE_FROM_3B_TO_2B[x + 4]], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x # Integer" - "\n%1 = Constant(4) # Integer" - "\n%2 = Add(0, 1) # Integer" - "\n%3 = TLU(2) # Integer" - "\n%4 = TLU(3) # Integer" + "%0 = x " + "# EncryptedScalar>" + "\n%1 = Constant(4) " + "# ClearScalar>" + "\n%2 = Add(0, 1) " + "# EncryptedScalar>" + "\n%3 = TLU(2) " + "# EncryptedScalar>" + "\n%4 = TLU(3) " + "# EncryptedScalar>" "\nreturn(%4)\n", ), ], From b924ffa61a2f54ae5058f282e594c73351d6492f Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 6 Sep 2021 11:55:21 +0200 Subject: [PATCH 0189/1104] chore: add a PR template closes #293 --- .../PULL_REQUEST_TEMPLATE/pull_request_template.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..54b1a07b5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,12 @@ +--- +name: Pull Request +about: Create a Pull Request. +--- + + From 959328e0f5d5b3bfe65494a97ca11d8398a93baf Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 6 Sep 2021 10:38:44 +0200 Subject: [PATCH 0190/1104] chore: rename hdk to concrete first pass --- .github/workflows/package-watcher.yaml | 3 +- .gitignore | 2 +- Makefile | 22 +++++++------ README.md | 6 ++-- benchmarks/test_compilation_and_evaluation.py | 2 +- {hdk => concrete}/__init__.py | 0 {hdk => concrete}/common/__init__.py | 0 .../common/bounds_measurement/__init__.py | 0 .../common/bounds_measurement/dataset_eval.py | 0 {hdk => concrete}/common/common_helpers.py | 0 .../common/compilation/__init__.py | 0 .../common/compilation/artifacts.py | 0 .../common/compilation/configuration.py | 0 .../common/data_types/__init__.py | 0 {hdk => concrete}/common/data_types/base.py | 0 .../common/data_types/dtypes_helpers.py | 0 {hdk => concrete}/common/data_types/floats.py | 0 .../common/data_types/integers.py | 0 .../common/debugging/__init__.py | 0 {hdk => concrete}/common/debugging/drawing.py | 0 .../common/debugging/printing.py | 0 .../common/extensions/__init__.py | 0 {hdk => concrete}/common/extensions/table.py | 0 {hdk => concrete}/common/mlir/__init__.py | 0 {hdk => concrete}/common/mlir/converters.py | 2 +- .../common/mlir/mlir_converter.py | 10 +++--- {hdk => concrete}/common/mlir/utils.py | 0 {hdk => concrete}/common/operator_graph.py | 0 .../common/optimization/__init__.py | 0 .../common/optimization/topological.py | 0 .../common/representation/__init__.py | 0 .../common/representation/intermediate.py | 0 {hdk => concrete}/common/tracing/__init__.py | 0 .../common/tracing/base_tracer.py | 0 .../common/tracing/tracing_helpers.py | 0 {hdk => concrete}/common/values/__init__.py | 0 {hdk => concrete}/common/values/base.py | 0 {hdk => concrete}/common/values/scalars.py | 0 {hdk => concrete}/common/values/tensors.py | 0 {hdk => concrete}/numpy/__init__.py | 0 {hdk => concrete}/numpy/compile.py | 0 {hdk => concrete}/numpy/np_dtypes_helpers.py | 12 +++---- {hdk => concrete}/numpy/tracing.py | 0 docker/Dockerfile.hdk-dev | 12 ++++--- docs/dev/COMPILATION.md | 10 +++--- docs/dev/FLOAT-FUSING.md | 2 +- docs/dev/GETTING-STARTED.md | 22 ++++++------- examples/QuantizedLinearRegression.ipynb | 6 ++-- examples/QuantizedLogisticRegression.ipynb | 6 ++-- pyproject.toml | 9 ++++-- .../container_timestamp_check.sh | 8 ++++- .../bounds_measurement/test_dataset_eval.py | 12 ++++--- tests/common/compilation/test_artifacts.py | 8 ++--- .../common/compilation/test_configuration.py | 8 ++--- .../common/data_types/test_dtypes_helpers.py | 10 +++--- tests/common/data_types/test_floats.py | 2 +- tests/common/data_types/test_integers.py | 2 +- tests/common/data_types/test_values.py | 8 ++--- tests/common/debugging/test_drawing.py | 8 ++--- tests/common/extensions/test_table.py | 12 +++---- tests/common/mlir/test_converters.py | 8 ++--- tests/common/mlir/test_mlir_converter.py | 32 +++++++++---------- .../common/optimization/test_float_fusing.py | 10 +++--- .../representation/test_intermediate.py | 13 +++++--- tests/common/test_common_helpers.py | 10 +++--- tests/common/tracing/test_tracing_helpers.py | 2 +- tests/conftest.py | 2 +- tests/numpy/test_compile.py | 12 +++---- tests/numpy/test_debugging.py | 10 +++--- tests/numpy/test_np_dtypes_helpers.py | 6 ++-- tests/numpy/test_tracing.py | 15 ++++++--- 71 files changed, 175 insertions(+), 149 deletions(-) rename {hdk => concrete}/__init__.py (100%) rename {hdk => concrete}/common/__init__.py (100%) rename {hdk => concrete}/common/bounds_measurement/__init__.py (100%) rename {hdk => concrete}/common/bounds_measurement/dataset_eval.py (100%) rename {hdk => concrete}/common/common_helpers.py (100%) rename {hdk => concrete}/common/compilation/__init__.py (100%) rename {hdk => concrete}/common/compilation/artifacts.py (100%) rename {hdk => concrete}/common/compilation/configuration.py (100%) rename {hdk => concrete}/common/data_types/__init__.py (100%) rename {hdk => concrete}/common/data_types/base.py (100%) rename {hdk => concrete}/common/data_types/dtypes_helpers.py (100%) rename {hdk => concrete}/common/data_types/floats.py (100%) rename {hdk => concrete}/common/data_types/integers.py (100%) rename {hdk => concrete}/common/debugging/__init__.py (100%) rename {hdk => concrete}/common/debugging/drawing.py (100%) rename {hdk => concrete}/common/debugging/printing.py (100%) rename {hdk => concrete}/common/extensions/__init__.py (100%) rename {hdk => concrete}/common/extensions/table.py (100%) rename {hdk => concrete}/common/mlir/__init__.py (100%) rename {hdk => concrete}/common/mlir/converters.py (99%) rename {hdk => concrete}/common/mlir/mlir_converter.py (95%) rename {hdk => concrete}/common/mlir/utils.py (100%) rename {hdk => concrete}/common/operator_graph.py (100%) rename {hdk => concrete}/common/optimization/__init__.py (100%) rename {hdk => concrete}/common/optimization/topological.py (100%) rename {hdk => concrete}/common/representation/__init__.py (100%) rename {hdk => concrete}/common/representation/intermediate.py (100%) rename {hdk => concrete}/common/tracing/__init__.py (100%) rename {hdk => concrete}/common/tracing/base_tracer.py (100%) rename {hdk => concrete}/common/tracing/tracing_helpers.py (100%) rename {hdk => concrete}/common/values/__init__.py (100%) rename {hdk => concrete}/common/values/base.py (100%) rename {hdk => concrete}/common/values/scalars.py (100%) rename {hdk => concrete}/common/values/tensors.py (100%) rename {hdk => concrete}/numpy/__init__.py (100%) rename {hdk => concrete}/numpy/compile.py (100%) rename {hdk => concrete}/numpy/np_dtypes_helpers.py (95%) rename {hdk => concrete}/numpy/tracing.py (100%) diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index d72e6c2fd..8ca998ee7 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -21,4 +21,5 @@ jobs: https://api.github.com/orgs/zama-ai/packages/container/zamalang-compiler/versions \ --env_img_url \ https://api.github.com/orgs/zama-ai/packages/container/hdk-env/versions \ - --token ${{ secrets.BOT_TOKEN }} + --token ${{ secrets.BOT_TOKEN }} \ + --org-repo ${{ github.repository }} diff --git a/.gitignore b/.gitignore index d187158c0..5609029c7 100644 --- a/.gitignore +++ b/.gitignore @@ -135,5 +135,5 @@ dmypy.json # pytest-benchmark results .benchmarks -# HDK compilation artifacts +# concrete compilation artifacts .artifacts diff --git a/Makefile b/Makefile index 7f300a877..83338dfc4 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ SHELL:=/bin/bash DEV_DOCKER_IMG:=hdk:dev DEV_DOCKERFILE:=docker/Dockerfile.hdk-dev +SRC_DIR:=concrete setup_env: poetry install @@ -17,12 +18,12 @@ sync_env: python_format: poetry run env bash ./script/source_format/format_python.sh \ - --dir hdk --dir tests --dir benchmarks + --dir $(SRC_DIR) --dir tests --dir benchmarks .PHONY: python_format check_python_format: poetry run env bash ./script/source_format/format_python.sh \ - --dir hdk --dir tests --dir benchmarks --check + --dir $(SRC_DIR) --dir tests --dir benchmarks --check .PHONY: check_python_format check_strip_nb: @@ -34,7 +35,7 @@ pylint: .PHONY: pylint pylint_src: - poetry run pylint --rcfile=pylintrc hdk + poetry run pylint --rcfile=pylintrc $(SRC_DIR) .PHONY: pylint_src pylint_tests: @@ -49,7 +50,7 @@ pylint_benchmarks: flake8: poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" \ - hdk/ tests/ benchmarks/ + $(SRC_DIR)/ tests/ benchmarks/ .PHONY: flake8 python_linting: pylint flake8 @@ -67,18 +68,19 @@ pcc_internal: check_python_format check_strip_nb python_linting mypy_ci pydocsty .PHONY: pcc_internal pytest: - poetry run pytest -svv --cov=hdk --cov-report=term-missing:skip-covered --cov-report=xml tests/ + poetry run pytest -svv \ + --cov=$(SRC_DIR) --cov-report=term-missing:skip-covered --cov-report=xml tests/ .PHONY: pytest # Not a huge fan of ignoring missing imports, but some packages do not have typing stubs mypy: - poetry run mypy -p hdk --ignore-missing-imports + poetry run mypy -p $(SRC_DIR) --ignore-missing-imports .PHONY: mypy # Friendly target to run mypy without ignoring missing stubs and still have errors messages # Allows to see which stubs we are missing mypy_ns: - poetry run mypy -p hdk + poetry run mypy -p $(SRC_DIR) .PHONY: mypy_ns mypy_test: @@ -118,7 +120,7 @@ docker_start: docker run --rm -it \ -p 8888:8888 \ --env DISPLAY=host.docker.internal:0 \ - --volume /"$$(pwd)":/hdk \ + --volume /"$$(pwd)":/src \ $(DEV_DOCKER_IMG) .PHONY: docker_start @@ -130,7 +132,7 @@ docker_bas: docker_build_and_start docs: clean_docs @# Generate the auto summary of documentations - poetry run sphinx-apidoc -o docs/_apidoc hdk + poetry run sphinx-apidoc -o docs/_apidoc $(SRC_DIR) @# Docs cd docs && poetry run $(MAKE) html SPHINXOPTS='-W --keep-going' @@ -150,7 +152,7 @@ build_and_open_docs: clean_docs docs open_docs pydocstyle: @# From http://www.pydocstyle.org/en/stable/error_codes.html - poetry run pydocstyle hdk --convention google --add-ignore=D1,D202 + poetry run pydocstyle $(SRC_DIR) --convention google --add-ignore=D1,D202 .PHONY: pydocstyle strip_nb: diff --git a/README.md b/README.md index 5345e6cf5..7e4afbdba 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# hdk +# concretefhe -Homomorphic Development Framework - collection of tools to FHE all the things +Concrete Framework Python API - collection of tools to FHE all the things -Developers can take a look at [ARCHITECTURE.md](docs/dev/ARCHITECTURE.md) to get the big picture and [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to start developing. +Developers can take a look at [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to get all the relevant informations on the project. diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py index 5949fcf47..c1567ac21 100644 --- a/benchmarks/test_compilation_and_evaluation.py +++ b/benchmarks/test_compilation_and_evaluation.py @@ -4,7 +4,7 @@ import itertools import pytest -import hdk.numpy as hnp +import concrete.numpy as hnp @pytest.mark.parametrize( diff --git a/hdk/__init__.py b/concrete/__init__.py similarity index 100% rename from hdk/__init__.py rename to concrete/__init__.py diff --git a/hdk/common/__init__.py b/concrete/common/__init__.py similarity index 100% rename from hdk/common/__init__.py rename to concrete/common/__init__.py diff --git a/hdk/common/bounds_measurement/__init__.py b/concrete/common/bounds_measurement/__init__.py similarity index 100% rename from hdk/common/bounds_measurement/__init__.py rename to concrete/common/bounds_measurement/__init__.py diff --git a/hdk/common/bounds_measurement/dataset_eval.py b/concrete/common/bounds_measurement/dataset_eval.py similarity index 100% rename from hdk/common/bounds_measurement/dataset_eval.py rename to concrete/common/bounds_measurement/dataset_eval.py diff --git a/hdk/common/common_helpers.py b/concrete/common/common_helpers.py similarity index 100% rename from hdk/common/common_helpers.py rename to concrete/common/common_helpers.py diff --git a/hdk/common/compilation/__init__.py b/concrete/common/compilation/__init__.py similarity index 100% rename from hdk/common/compilation/__init__.py rename to concrete/common/compilation/__init__.py diff --git a/hdk/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py similarity index 100% rename from hdk/common/compilation/artifacts.py rename to concrete/common/compilation/artifacts.py diff --git a/hdk/common/compilation/configuration.py b/concrete/common/compilation/configuration.py similarity index 100% rename from hdk/common/compilation/configuration.py rename to concrete/common/compilation/configuration.py diff --git a/hdk/common/data_types/__init__.py b/concrete/common/data_types/__init__.py similarity index 100% rename from hdk/common/data_types/__init__.py rename to concrete/common/data_types/__init__.py diff --git a/hdk/common/data_types/base.py b/concrete/common/data_types/base.py similarity index 100% rename from hdk/common/data_types/base.py rename to concrete/common/data_types/base.py diff --git a/hdk/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py similarity index 100% rename from hdk/common/data_types/dtypes_helpers.py rename to concrete/common/data_types/dtypes_helpers.py diff --git a/hdk/common/data_types/floats.py b/concrete/common/data_types/floats.py similarity index 100% rename from hdk/common/data_types/floats.py rename to concrete/common/data_types/floats.py diff --git a/hdk/common/data_types/integers.py b/concrete/common/data_types/integers.py similarity index 100% rename from hdk/common/data_types/integers.py rename to concrete/common/data_types/integers.py diff --git a/hdk/common/debugging/__init__.py b/concrete/common/debugging/__init__.py similarity index 100% rename from hdk/common/debugging/__init__.py rename to concrete/common/debugging/__init__.py diff --git a/hdk/common/debugging/drawing.py b/concrete/common/debugging/drawing.py similarity index 100% rename from hdk/common/debugging/drawing.py rename to concrete/common/debugging/drawing.py diff --git a/hdk/common/debugging/printing.py b/concrete/common/debugging/printing.py similarity index 100% rename from hdk/common/debugging/printing.py rename to concrete/common/debugging/printing.py diff --git a/hdk/common/extensions/__init__.py b/concrete/common/extensions/__init__.py similarity index 100% rename from hdk/common/extensions/__init__.py rename to concrete/common/extensions/__init__.py diff --git a/hdk/common/extensions/table.py b/concrete/common/extensions/table.py similarity index 100% rename from hdk/common/extensions/table.py rename to concrete/common/extensions/table.py diff --git a/hdk/common/mlir/__init__.py b/concrete/common/mlir/__init__.py similarity index 100% rename from hdk/common/mlir/__init__.py rename to concrete/common/mlir/__init__.py diff --git a/hdk/common/mlir/converters.py b/concrete/common/mlir/converters.py similarity index 99% rename from hdk/common/mlir/converters.py rename to concrete/common/mlir/converters.py index a457253da..d5ab22fee 100644 --- a/hdk/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -1,4 +1,4 @@ -"""Converter functions from HDKIR to MLIR. +"""Converter functions from the common IR to MLIR. Converter functions all have the same signature `converter(node, preds, ir_to_mlir_node, ctx)` - `node`: IntermediateNode to be converted diff --git a/hdk/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py similarity index 95% rename from hdk/common/mlir/mlir_converter.py rename to concrete/common/mlir/mlir_converter.py index 7c50c0988..352927ba7 100644 --- a/hdk/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -30,13 +30,13 @@ from ..representation import intermediate as ir class MLIRConverter: - """Converter of the HDKIR to MLIR.""" + """Converter of the common IR to MLIR.""" def __init__(self, conversion_functions: dict) -> None: """Instantiate a converter with a given set of converters. Args: - conversion_functions (dict): mapping HDKIR nodes to functions that generate MLIR. + conversion_functions (dict): mapping common IR nodes to functions that generate MLIR. every function should have 4 arguments: - node (IntermediateNode): the node itself to be converted - operands (IntermediateNode): predecessors of node ordered as operands @@ -97,8 +97,8 @@ class MLIRConverter: # unsigned integer are considered signless in the compiler return IntegerType.get_signless(bit_width) - def hdk_value_to_mlir_type(self, value: values.BaseValue) -> MLIRType: - """Convert an HDK value to its corresponding MLIR Type. + def common_value_to_mlir_type(self, value: values.BaseValue) -> MLIRType: + """Convert a common value to its corresponding MLIR Type. Args: value: value to convert @@ -147,7 +147,7 @@ class MLIRConverter: # collect inputs with InsertionPoint(module.body): func_types = [ - self.hdk_value_to_mlir_type(input_node.inputs[0]) + self.common_value_to_mlir_type(input_node.inputs[0]) for input_node in op_graph.get_ordered_inputs() ] diff --git a/hdk/common/mlir/utils.py b/concrete/common/mlir/utils.py similarity index 100% rename from hdk/common/mlir/utils.py rename to concrete/common/mlir/utils.py diff --git a/hdk/common/operator_graph.py b/concrete/common/operator_graph.py similarity index 100% rename from hdk/common/operator_graph.py rename to concrete/common/operator_graph.py diff --git a/hdk/common/optimization/__init__.py b/concrete/common/optimization/__init__.py similarity index 100% rename from hdk/common/optimization/__init__.py rename to concrete/common/optimization/__init__.py diff --git a/hdk/common/optimization/topological.py b/concrete/common/optimization/topological.py similarity index 100% rename from hdk/common/optimization/topological.py rename to concrete/common/optimization/topological.py diff --git a/hdk/common/representation/__init__.py b/concrete/common/representation/__init__.py similarity index 100% rename from hdk/common/representation/__init__.py rename to concrete/common/representation/__init__.py diff --git a/hdk/common/representation/intermediate.py b/concrete/common/representation/intermediate.py similarity index 100% rename from hdk/common/representation/intermediate.py rename to concrete/common/representation/intermediate.py diff --git a/hdk/common/tracing/__init__.py b/concrete/common/tracing/__init__.py similarity index 100% rename from hdk/common/tracing/__init__.py rename to concrete/common/tracing/__init__.py diff --git a/hdk/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py similarity index 100% rename from hdk/common/tracing/base_tracer.py rename to concrete/common/tracing/base_tracer.py diff --git a/hdk/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py similarity index 100% rename from hdk/common/tracing/tracing_helpers.py rename to concrete/common/tracing/tracing_helpers.py diff --git a/hdk/common/values/__init__.py b/concrete/common/values/__init__.py similarity index 100% rename from hdk/common/values/__init__.py rename to concrete/common/values/__init__.py diff --git a/hdk/common/values/base.py b/concrete/common/values/base.py similarity index 100% rename from hdk/common/values/base.py rename to concrete/common/values/base.py diff --git a/hdk/common/values/scalars.py b/concrete/common/values/scalars.py similarity index 100% rename from hdk/common/values/scalars.py rename to concrete/common/values/scalars.py diff --git a/hdk/common/values/tensors.py b/concrete/common/values/tensors.py similarity index 100% rename from hdk/common/values/tensors.py rename to concrete/common/values/tensors.py diff --git a/hdk/numpy/__init__.py b/concrete/numpy/__init__.py similarity index 100% rename from hdk/numpy/__init__.py rename to concrete/numpy/__init__.py diff --git a/hdk/numpy/compile.py b/concrete/numpy/compile.py similarity index 100% rename from hdk/numpy/compile.py rename to concrete/numpy/compile.py diff --git a/hdk/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py similarity index 95% rename from hdk/numpy/np_dtypes_helpers.py rename to concrete/numpy/np_dtypes_helpers.py index d755bcbcc..cbba8f9df 100644 --- a/hdk/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -17,7 +17,7 @@ from ..common.data_types.floats import Float from ..common.data_types.integers import Integer from ..common.values import BaseValue, ScalarValue, TensorValue -NUMPY_TO_HDK_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { +NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.int32): Integer(32, is_signed=True), numpy.dtype(numpy.int64): Integer(64, is_signed=True), numpy.dtype(numpy.uint32): Integer(32, is_signed=False), @@ -26,8 +26,8 @@ NUMPY_TO_HDK_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.float64): Float(64), } -SUPPORTED_NUMPY_DTYPES = tuple(NUMPY_TO_HDK_DTYPE_MAPPING) -SUPPORTED_NUMPY_DTYPES_CLASS_TYPES = tuple(dtype.type for dtype in NUMPY_TO_HDK_DTYPE_MAPPING) +SUPPORTED_NUMPY_DTYPES = tuple(NUMPY_TO_COMMON_DTYPE_MAPPING) +SUPPORTED_NUMPY_DTYPES_CLASS_TYPES = tuple(dtype.type for dtype in NUMPY_TO_COMMON_DTYPE_MAPPING) SUPPORTED_DTYPE_MSG_STRING = ", ".join(sorted(str(dtype) for dtype in SUPPORTED_NUMPY_DTYPES)) @@ -46,9 +46,9 @@ def convert_numpy_dtype_to_base_data_type(numpy_dtype: DTypeLike) -> BaseDataTyp """ # Normalize numpy_dtype normalized_numpy_dtype = numpy.dtype(numpy_dtype) - corresponding_hdk_dtype = NUMPY_TO_HDK_DTYPE_MAPPING.get(normalized_numpy_dtype, None) + corresponding_common_dtype = NUMPY_TO_COMMON_DTYPE_MAPPING.get(normalized_numpy_dtype, None) - if corresponding_hdk_dtype is None: + if corresponding_common_dtype is None: raise ValueError( f"Unsupported numpy type: {numpy_dtype} ({normalized_numpy_dtype}), " f"supported numpy types: " @@ -56,7 +56,7 @@ def convert_numpy_dtype_to_base_data_type(numpy_dtype: DTypeLike) -> BaseDataTyp ) # deepcopy to avoid having the value from the dict modified - return deepcopy(corresponding_hdk_dtype) + return deepcopy(corresponding_common_dtype) def convert_base_data_type_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.dtype: diff --git a/hdk/numpy/tracing.py b/concrete/numpy/tracing.py similarity index 100% rename from hdk/numpy/tracing.py rename to concrete/numpy/tracing.py diff --git a/docker/Dockerfile.hdk-dev b/docker/Dockerfile.hdk-dev index 85652c972..bd5566383 100644 --- a/docker/Dockerfile.hdk-dev +++ b/docker/Dockerfile.hdk-dev @@ -1,14 +1,16 @@ FROM ghcr.io/zama-ai/hdk-env -RUN echo "source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ +ENV SRC_DIR_NAME=src + +RUN echo "source /${SRC_DIR_NAME}/.docker_venv/bin/activate" >> /root/.bashrc && \ echo "if [[ \"\$?\" != \"0\" ]]; then" >> /root/.bashrc && \ - echo " python3 -m venv /hdk/.docker_venv" >> /root/.bashrc && \ - echo " source /hdk/.docker_venv/bin/activate" >> /root/.bashrc && \ - echo " cd /hdk/ && make setup_env" >> /root/.bashrc && \ + echo " python3 -m venv /${SRC_DIR_NAME}/.docker_venv" >> /root/.bashrc && \ + echo " source /${SRC_DIR_NAME}/.docker_venv/bin/activate" >> /root/.bashrc && \ + echo " cd /${SRC_DIR_NAME}/ && make setup_env" >> /root/.bashrc && \ echo "fi" >> /root/.bashrc && \ echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc && \ echo "export MPLBACKEND=TkAgg" >> /root/.bashrc -WORKDIR /hdk +WORKDIR /${SRC_DIR_NAME} ENTRYPOINT ["/bin/bash", "-l"] diff --git a/docs/dev/COMPILATION.md b/docs/dev/COMPILATION.md index bc6bb55e7..32b8ca63e 100644 --- a/docs/dev/COMPILATION.md +++ b/docs/dev/COMPILATION.md @@ -1,8 +1,8 @@ # Compilation Pipeline In Depth -## What is HDK? +## What is concretefhe? -`HDK` is a framework for developing homomorphic applications. +`concretefhe` is the python API of the `concrete` framework for developing homomorphic applications. One of its essential functionalities is to transform Python functions to their `MLIR` equivalent. Unfortunately, not all python functions can be converted due to the limits of current product (we are in the alpha stage), or sometimes due to inherent restrictions of FHE itself. However, one can already build interesting and impressing use cases, and more will be available in further versions of the framework. @@ -10,8 +10,8 @@ However, one can already build interesting and impressing use cases, and more wi ## How can I use it? ```python -# Import necessary HDK components -import hdk.numpy as hnp +# Import necessary concrete components +import concrete.numpy as hnp # Define the function to homomorphize def f(x, y): @@ -104,7 +104,7 @@ Tracing is also responsible for indicating whether the values in the node would The goal of topological transforms is to make more functions compilable. -With the current version of `HDK` floating point inputs and floating point outputs are not supported. +With the current version of `concrete` floating point inputs and floating point outputs are not supported. However, if the floating points operations are intermediate operations, they can sometimes be fused into a single table lookup from integer to integer thanks to some specific transforms. Let's take a closer look at the transforms we perform today. diff --git a/docs/dev/FLOAT-FUSING.md b/docs/dev/FLOAT-FUSING.md index cd5140aa8..b0c833b0d 100644 --- a/docs/dev/FLOAT-FUSING.md +++ b/docs/dev/FLOAT-FUSING.md @@ -34,7 +34,7 @@ The simplified graph of operations with the float subgraph condensed in an `Arbi ![](../_static/float_fusing_example/after.png) -## How is it done in HDK? +## How is it done in concretefhe? The first step consists in detecting where we go from floating point computation back to integers. This allows to identify the potential terminal node of the float subgraph we are going to fuse. diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md index fffd75179..f017f9e54 100644 --- a/docs/dev/GETTING-STARTED.md +++ b/docs/dev/GETTING-STARTED.md @@ -2,11 +2,11 @@ ## Preparation -Before you can start improving `hdk` you need to set up your development environment! This section will show how you can do that. +Before you can start improving `concretefhe` you need to set up your development environment! This section will show how you can do that. ### Installing Python v3.8 -`hdk` is a `Python` library. So `Python` should be installed to develop `hdk`. `v3.8` is recommended because our CI also uses `v3.8`. +`concretefhe` is a `Python` library. So `Python` should be installed to develop `concretefhe`. `v3.8` is recommended because our CI also uses `v3.8`. You can follow [this](https://realpython.com/installing-python/) guide to install it (alternatively you can google `how to install python 3.8`). @@ -41,10 +41,10 @@ On Windows check [this GitHub gist](https://gist.github.com/evanwill/0207876c324 ### Cloning repository -Now, it's time to get the source code of `hdk`. You can use the following command to do that. +Now, it's time to get the source code of `concretefhe`. You can use the following command to do that. ```shell -git clone https://github.com/zama-ai/hdk.git +git clone https://github.com/zama-ai/concretefhe-internal.git ``` ### Setting up environment @@ -52,7 +52,7 @@ git clone https://github.com/zama-ai/hdk.git We are going to make use of virtual environments. This helps to keep the project isolated from other `Python` projects in the system. The following commands will create a new virtual environment under the project directory and install dependencies to it. ```shell -cd hdk +cd concrete make setup_env ``` @@ -95,9 +95,9 @@ In this section we will go over some terms that we use throughout the project. ## Module Structure -In this section, we will discuss the module structure of hdk briefly. You are encouraged to check individual `.py` files to learn more! +In this section, we will discuss the module structure of concretefhe briefly. You are encouraged to check individual `.py` files to learn more! -- hdk +- concrete - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) - bounds_measurement: utilities for determining bounds of intermediate representation - compilation: type definitions related to compilation (e.g., compilation config, compilation artifacts) @@ -106,7 +106,7 @@ In this section, we will discuss the module structure of hdk briefly. You are en - extensions: utilities that provide special functionality to our users - representation: type definitions of intermediate representation - tracing: utilities for generic function tracing used during intermediate representation creation - - numpy: numpy frontend of hdk + - numpy: numpy frontend of concrete ## Working in Docker @@ -140,7 +140,7 @@ Install Xming and use Xlaunch: ### Logging in and building the image -Docker image of `hdk` is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. +Docker image of `concretefhe` is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. Upon joining to the team, you need to log in using the following command: @@ -162,7 +162,7 @@ After you finish your work, you can leave the docker by using the `exit` command Now, you have a working environment, and you know what is where in the project. -There are two ways to contribute to HDK: +There are two ways to contribute to `concretefhe`: - you can open issues to report bugs, typos and suggest ideas - you can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests, so please make sure to get in touch before you do! @@ -185,7 +185,7 @@ git checkout -b fix/tracing_indexing_42 ### Before committing -Each commit to `hdk` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. +Each commit to `concretefhe` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. ```shell make -k pcc diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index 4a5e42d5c..bfae7b759 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -5,7 +5,7 @@ "source": [ "# Quantized Linear Regression\n", "\n", - "Currently, **hdk** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" + "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" ], "metadata": {} }, @@ -547,7 +547,7 @@ { "cell_type": "markdown", "source": [ - "### Let's import the hdk numpy package now!" + "### Let's import the concrete numpy package now!" ], "metadata": {} }, @@ -555,7 +555,7 @@ "cell_type": "code", "execution_count": null, "source": [ - "import hdk.numpy as hnp" + "import concrete.numpy as hnp" ], "outputs": [], "metadata": {} diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 6fe14dd2b..81c777764 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -5,7 +5,7 @@ "source": [ "# Quantized Logistic Regression\n", "\n", - "Currently, **hdk** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" + "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" ], "metadata": {} }, @@ -642,7 +642,7 @@ { "cell_type": "markdown", "source": [ - "### Let's import the hdk numpy package now!" + "### Let's import the concrete numpy package now!" ], "metadata": {} }, @@ -650,7 +650,7 @@ "cell_type": "code", "execution_count": null, "source": [ - "import hdk.numpy as hnp" + "import concrete.numpy as hnp" ], "outputs": [], "metadata": {} diff --git a/pyproject.toml b/pyproject.toml index 310462a89..a26855618 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,11 @@ [tool.poetry] -name = "hdk" +name = "concretefhe" version = "0.1.0" -description = "Zama Homomorphic Development frameworK" -authors = ["Arthur Meyre "] +description = "Concrete Framework Python API" +authors = ["A. Meyre", "U. Sahin", "A. Benaissa", "B. Chevallier", "Zama Team"] +packages = [ + { include = "concrete" }, +] [tool.poetry.dependencies] python = ">=3.7,<3.10" diff --git a/script/actions_utils/container_timestamp_check.sh b/script/actions_utils/container_timestamp_check.sh index 989dfb689..3c52a1b5d 100755 --- a/script/actions_utils/container_timestamp_check.sh +++ b/script/actions_utils/container_timestamp_check.sh @@ -5,6 +5,7 @@ set -e BASE_IMG_ENDPOINT_URL= ENV_IMG_ENDPOINT_URL= TOKEN= +ORG_REPO= while [ -n "$1" ] do @@ -24,6 +25,11 @@ do TOKEN="$1" ;; + "--org-repo" ) + shift + ORG_REPO="$1" + ;; + *) echo "Unknown param : $1" exit -1 @@ -63,7 +69,7 @@ if [[ "${BASE_IMG_DATE}" -ge "${ENV_IMG_DATE}" ]]; then -X POST \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${TOKEN}" \ - https://api.github.com/repos/zama-ai/hdk/dispatches \ + https://api.github.com/repos/${ORG_REPO}/dispatches \ -d '{"event_type":"rebuild-docker"}' else echo "Image up to date, nothing to do." diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_dataset_eval.py index 50083fbf5..e22687d95 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_dataset_eval.py @@ -4,11 +4,13 @@ from typing import Tuple import pytest -from hdk.common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.common.values import EncryptedScalar -from hdk.numpy.tracing import trace_numpy_function +from concrete.common.bounds_measurement.dataset_eval import ( + eval_op_graph_bounds_on_dataset, +) +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.common.values import EncryptedScalar +from concrete.numpy.tracing import trace_numpy_function @pytest.mark.parametrize( diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 78f7d4e49..64cc695b5 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -3,10 +3,10 @@ import tempfile from pathlib import Path -from hdk.common.compilation import CompilationArtifacts -from hdk.common.data_types.integers import UnsignedInteger -from hdk.common.values import EncryptedScalar -from hdk.numpy.compile import compile_numpy_function +from concrete.common.compilation import CompilationArtifacts +from concrete.common.data_types.integers import UnsignedInteger +from concrete.common.values import EncryptedScalar +from concrete.numpy.compile import compile_numpy_function def test_artifacts_export(): diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index aa37d3d38..c8619854b 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -5,10 +5,10 @@ from inspect import signature import numpy import pytest -from hdk.common.compilation import CompilationConfiguration -from hdk.common.data_types.integers import Integer -from hdk.common.values import EncryptedScalar -from hdk.numpy.compile import compile_numpy_function_into_op_graph +from concrete.common.compilation import CompilationConfiguration +from concrete.common.data_types.integers import Integer +from concrete.common.values import EncryptedScalar +from concrete.numpy.compile import compile_numpy_function_into_op_graph def no_fuse(x): diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 64da36152..74413738d 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -2,16 +2,16 @@ import pytest -from hdk.common.data_types.base import BaseDataType -from hdk.common.data_types.dtypes_helpers import ( +from concrete.common.data_types.base import BaseDataType +from concrete.common.data_types.dtypes_helpers import ( find_type_to_hold_both_lossy, mix_values_determine_holding_dtype, value_is_encrypted_scalar_integer, value_is_encrypted_scalar_unsigned_integer, ) -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.common.values import ( +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.common.values import ( BaseValue, ClearScalar, ClearTensor, diff --git a/tests/common/data_types/test_floats.py b/tests/common/data_types/test_floats.py index 49a7ba380..fc33acce5 100644 --- a/tests/common/data_types/test_floats.py +++ b/tests/common/data_types/test_floats.py @@ -3,7 +3,7 @@ import pytest -from hdk.common.data_types.floats import Float, Float32, Float64 +from concrete.common.data_types.floats import Float, Float32, Float64 @pytest.mark.parametrize( diff --git a/tests/common/data_types/test_integers.py b/tests/common/data_types/test_integers.py index 7d1f70a2b..59c186b29 100644 --- a/tests/common/data_types/test_integers.py +++ b/tests/common/data_types/test_integers.py @@ -4,7 +4,7 @@ import random import pytest -from hdk.common.data_types.integers import ( +from concrete.common.data_types.integers import ( Integer, SignedInteger, UnsignedInteger, diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py index 7359a3760..d242fc27b 100644 --- a/tests/common/data_types/test_values.py +++ b/tests/common/data_types/test_values.py @@ -6,10 +6,10 @@ from typing import Callable, Optional, Tuple, Union import pytest -from hdk.common.data_types.base import BaseDataType -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.common.values import ClearTensor, EncryptedTensor, TensorValue +from concrete.common.data_types.base import BaseDataType +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.common.values import ClearTensor, EncryptedTensor, TensorValue class DummyDtype(BaseDataType): diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py index 8dd3ffebc..4155cbb94 100644 --- a/tests/common/debugging/test_drawing.py +++ b/tests/common/debugging/test_drawing.py @@ -3,10 +3,10 @@ import tempfile from pathlib import Path -from hdk.common.data_types.integers import Integer -from hdk.common.debugging import draw_graph -from hdk.common.values import EncryptedScalar -from hdk.numpy.compile import compile_numpy_function_into_op_graph +from concrete.common.data_types.integers import Integer +from concrete.common.debugging import draw_graph +from concrete.common.values import EncryptedScalar +from concrete.numpy.compile import compile_numpy_function_into_op_graph def test_draw_graph_with_saving(): diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 1e9d51f0f..81e7e90ca 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -5,12 +5,12 @@ from copy import deepcopy import networkx as nx import pytest -from hdk.common import is_a_power_of_2 -from hdk.common.data_types.integers import Integer -from hdk.common.extensions.table import LookupTable -from hdk.common.representation import intermediate as ir -from hdk.common.values import EncryptedScalar -from hdk.numpy import tracing +from concrete.common import is_a_power_of_2 +from concrete.common.data_types.integers import Integer +from concrete.common.extensions.table import LookupTable +from concrete.common.representation import intermediate as ir +from concrete.common.values import EncryptedScalar +from concrete.numpy import tracing def test_lookup_table_size_constraints(): diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index 04ce27223..0f00e6c6c 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -1,10 +1,10 @@ """Test converter functions""" import pytest -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.common.mlir.converters import add, apply_lut, constant, dot, mul, sub -from hdk.common.values import ClearScalar, EncryptedScalar +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.common.mlir.converters import add, apply_lut, constant, dot, mul, sub +from concrete.common.values import ClearScalar, EncryptedScalar class MockNode: diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 1b45ce0d7..cca5d84db 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -8,12 +8,12 @@ from mlir.ir import IntegerType, Location, RankedTensorType, UnrankedTensorType from zamalang import compiler from zamalang.dialects import hlfhe -from hdk.common.data_types.integers import Integer -from hdk.common.extensions.table import LookupTable -from hdk.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter -from hdk.common.values import ClearScalar, EncryptedScalar -from hdk.common.values.tensors import ClearTensor, EncryptedTensor -from hdk.numpy.compile import compile_numpy_function_into_op_graph +from concrete.common.data_types.integers import Integer +from concrete.common.extensions.table import LookupTable +from concrete.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter +from concrete.common.values import ClearScalar, EncryptedScalar +from concrete.common.values.tensors import ClearTensor, EncryptedTensor +from concrete.numpy.compile import compile_numpy_function_into_op_graph def add(x, y): @@ -212,21 +212,21 @@ def test_mlir_converter(func, args_dict, args_ranges): compiler.round_trip(mlir_result) -def test_hdk_encrypted_integer_to_mlir_type(): +def test_concrete_encrypted_integer_to_mlir_type(): """Test conversion of EncryptedScalar into MLIR""" value = EncryptedScalar(Integer(7, is_signed=False)) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) - eint = converter.hdk_value_to_mlir_type(value) + eint = converter.common_value_to_mlir_type(value) assert eint == hlfhe.EncryptedIntegerType.get(converter.context, 7) @pytest.mark.parametrize("is_signed", [True, False]) -def test_hdk_clear_integer_to_mlir_type(is_signed): +def test_concrete_clear_integer_to_mlir_type(is_signed): """Test conversion of ClearScalar into MLIR""" value = ClearScalar(Integer(5, is_signed=is_signed)) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with converter.context: - int_mlir = converter.hdk_value_to_mlir_type(value) + int_mlir = converter.common_value_to_mlir_type(value) if is_signed: assert int_mlir == IntegerType.get_signed(5) else: @@ -243,12 +243,12 @@ def test_hdk_clear_integer_to_mlir_type(is_signed): (-1, 5), ], ) -def test_hdk_clear_tensor_integer_to_mlir_type(is_signed, shape): +def test_concrete_clear_tensor_integer_to_mlir_type(is_signed, shape): """Test conversion of ClearTensor into MLIR""" value = ClearTensor(Integer(5, is_signed=is_signed), shape) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with converter.context, Location.unknown(): - tensor_mlir = converter.hdk_value_to_mlir_type(value) + tensor_mlir = converter.common_value_to_mlir_type(value) if is_signed: element_type = IntegerType.get_signed(5) else: @@ -269,12 +269,12 @@ def test_hdk_clear_tensor_integer_to_mlir_type(is_signed, shape): (-1, 5), ], ) -def test_hdk_encrypted_tensor_integer_to_mlir_type(shape): +def test_concrete_encrypted_tensor_integer_to_mlir_type(shape): """Test conversion of EncryptedTensor into MLIR""" value = EncryptedTensor(Integer(6, is_signed=False), shape) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with converter.context, Location.unknown(): - tensor_mlir = converter.hdk_value_to_mlir_type(value) + tensor_mlir = converter.common_value_to_mlir_type(value) element_type = hlfhe.EncryptedIntegerType.get(converter.context, 6) if shape is None: expected_type = UnrankedTensorType.get(element_type) @@ -283,12 +283,12 @@ def test_hdk_encrypted_tensor_integer_to_mlir_type(shape): assert tensor_mlir == expected_type -def test_failing_hdk_to_mlir_type(): +def test_failing_concrete_to_mlir_type(): """Test failing conversion of an unsupported type into MLIR""" value = "random" converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with pytest.raises(TypeError, match=r"can't convert value of type .* to MLIR type"): - converter.hdk_value_to_mlir_type(value) + converter.common_value_to_mlir_type(value) # pylint: enable=no-name-in-module,no-member diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 65c06a751..57cef96d3 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -5,11 +5,11 @@ from inspect import signature import numpy import pytest -from hdk.common.data_types.integers import Integer -from hdk.common.optimization.topological import fuse_float_operations -from hdk.common.values import EncryptedScalar -from hdk.numpy import tracing -from hdk.numpy.tracing import trace_numpy_function +from concrete.common.data_types.integers import Integer +from concrete.common.optimization.topological import fuse_float_operations +from concrete.common.values import EncryptedScalar +from concrete.numpy import tracing +from concrete.numpy.tracing import trace_numpy_function def no_fuse(x): diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index dcab16b77..b99668bac 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -3,10 +3,15 @@ import numpy import pytest -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.common.representation import intermediate as ir -from hdk.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.common.representation import intermediate as ir +from concrete.common.values import ( + ClearScalar, + ClearTensor, + EncryptedScalar, + EncryptedTensor, +) @pytest.mark.parametrize( diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index 81e0c2c4e..deccf46b2 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -4,11 +4,11 @@ from copy import deepcopy import pytest -from hdk.common import check_op_graph_is_integer_program, is_a_power_of_2 -from hdk.common.data_types.floats import Float64 -from hdk.common.data_types.integers import Integer -from hdk.common.values import EncryptedScalar -from hdk.numpy.tracing import trace_numpy_function +from concrete.common import check_op_graph_is_integer_program, is_a_power_of_2 +from concrete.common.data_types.floats import Float64 +from concrete.common.data_types.integers import Integer +from concrete.common.values import EncryptedScalar +from concrete.numpy.tracing import trace_numpy_function @pytest.mark.parametrize( diff --git a/tests/common/tracing/test_tracing_helpers.py b/tests/common/tracing/test_tracing_helpers.py index 36996cb47..3cabf6ce4 100644 --- a/tests/common/tracing/test_tracing_helpers.py +++ b/tests/common/tracing/test_tracing_helpers.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List import pytest -from hdk.common.tracing.tracing_helpers import prepare_function_parameters +from concrete.common.tracing.tracing_helpers import prepare_function_parameters @pytest.mark.parametrize( diff --git a/tests/conftest.py b/tests/conftest.py index 9454fb98f..9df9f064c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import networkx as nx import networkx.algorithms.isomorphism as iso import pytest -from hdk.common.representation.intermediate import ( +from concrete.common.representation.intermediate import ( ALL_IR_NODES, Add, ArbitraryFunction, diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 447445f0c..d9ac7b3fd 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -5,12 +5,12 @@ import random import numpy import pytest -from hdk.common.compilation import CompilationConfiguration -from hdk.common.data_types.integers import Integer -from hdk.common.debugging import draw_graph, get_printable_graph -from hdk.common.extensions.table import LookupTable -from hdk.common.values import EncryptedScalar, EncryptedTensor -from hdk.numpy.compile import ( +from concrete.common.compilation import CompilationConfiguration +from concrete.common.data_types.integers import Integer +from concrete.common.debugging import draw_graph, get_printable_graph +from concrete.common.extensions.table import LookupTable +from concrete.common.values import EncryptedScalar, EncryptedTensor +from concrete.numpy.compile import ( compile_numpy_function, compile_numpy_function_into_op_graph, ) diff --git a/tests/numpy/test_debugging.py b/tests/numpy/test_debugging.py index 2b791c56e..3f32797ea 100644 --- a/tests/numpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -3,11 +3,11 @@ import numpy import pytest -from hdk.common.data_types.integers import Integer -from hdk.common.debugging import draw_graph, get_printable_graph -from hdk.common.extensions.table import LookupTable -from hdk.common.values import ClearScalar, EncryptedScalar, EncryptedTensor -from hdk.numpy import tracing +from concrete.common.data_types.integers import Integer +from concrete.common.debugging import draw_graph, get_printable_graph +from concrete.common.extensions.table import LookupTable +from concrete.common.values import ClearScalar, EncryptedScalar, EncryptedTensor +from concrete.numpy import tracing LOOKUP_TABLE_FROM_2B_TO_4B = LookupTable([9, 2, 4, 11]) LOOKUP_TABLE_FROM_3B_TO_2B = LookupTable([0, 1, 3, 2, 2, 3, 1, 0]) diff --git a/tests/numpy/test_np_dtypes_helpers.py b/tests/numpy/test_np_dtypes_helpers.py index bf97541b5..6961c714f 100644 --- a/tests/numpy/test_np_dtypes_helpers.py +++ b/tests/numpy/test_np_dtypes_helpers.py @@ -3,9 +3,9 @@ import numpy import pytest -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.numpy.np_dtypes_helpers import ( +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.numpy.np_dtypes_helpers import ( convert_base_data_type_to_numpy_dtype, convert_numpy_dtype_to_base_data_type, ) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 521c1bec6..1df0e01d0 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -4,11 +4,16 @@ import networkx as nx import numpy import pytest -from hdk.common.data_types.floats import Float -from hdk.common.data_types.integers import Integer -from hdk.common.representation import intermediate as ir -from hdk.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor -from hdk.numpy import tracing +from concrete.common.data_types.floats import Float +from concrete.common.data_types.integers import Integer +from concrete.common.representation import intermediate as ir +from concrete.common.values import ( + ClearScalar, + ClearTensor, + EncryptedScalar, + EncryptedTensor, +) +from concrete.numpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] From 98c25c5d8bba5e9f4f40188471d70483a9c20d55 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 7 Sep 2021 09:44:19 +0200 Subject: [PATCH 0191/1104] chore: rename packages for concretefhe --- .github/workflows/continuous-integration.yaml | 4 ++-- .github/workflows/docker-env.yaml | 12 ++++++------ .github/workflows/package-watcher.yaml | 2 +- Makefile | 4 ++-- ...Dockerfile.hdk-dev => Dockerfile.concretefhe-dev} | 2 +- ...Dockerfile.hdk-env => Dockerfile.concretefhe-env} | 0 6 files changed, 12 insertions(+), 12 deletions(-) rename docker/{Dockerfile.hdk-dev => Dockerfile.concretefhe-dev} (94%) rename docker/{Dockerfile.hdk-env => Dockerfile.concretefhe-env} (100%) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index fe97021cd..1f706025b 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -1,4 +1,4 @@ -name: HDK CI Pipeline +name: concretefhe CI Pipeline on: pull_request: push: @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 container: - image: ghcr.io/zama-ai/hdk-env + image: ghcr.io/zama-ai/concretefhe-env credentials: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index bf6a7b069..016754607 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -1,11 +1,11 @@ -name: Docker image (HDK dev/CI) +name: Docker image (concretefhe dev/CI) on: push: branches: - main paths: - - docker/Dockerfile.hdk-env + - docker/Dockerfile.concretefhe-env # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -21,10 +21,10 @@ jobs: group: ${{ github.ref }} cancel-in-progress: true - name: Build & Push the HDK env Docker Image + name: Build & Push the concretefhe env Docker Image runs-on: ubuntu-20.04 env: - IMAGE_URL: ghcr.io/zama-ai/hdk-env + IMAGE_URL: ghcr.io/zama-ai/concretefhe-env steps: - uses: actions/checkout@v2 @@ -38,13 +38,13 @@ jobs: registry: ghcr.io username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} - - name: Build hdk-env Image + - name: Build concretefhe-env Image if: ${{ success() && !cancelled() }} uses: docker/build-push-action@v2 with: context: . builder: ${{ steps.buildx.outputs.name }} - file: docker/Dockerfile.hdk-env + file: docker/Dockerfile.concretefhe-env push: true tags: "${{ env.IMAGE_URL }}:latest" no-cache: true diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index 8ca998ee7..a14f14044 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -20,6 +20,6 @@ jobs: --base_img_url \ https://api.github.com/orgs/zama-ai/packages/container/zamalang-compiler/versions \ --env_img_url \ - https://api.github.com/orgs/zama-ai/packages/container/hdk-env/versions \ + https://api.github.com/orgs/zama-ai/packages/container/concretefhe-env/versions \ --token ${{ secrets.BOT_TOKEN }} \ --org-repo ${{ github.repository }} diff --git a/Makefile b/Makefile index 83338dfc4..288770470 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL:=/bin/bash -DEV_DOCKER_IMG:=hdk:dev -DEV_DOCKERFILE:=docker/Dockerfile.hdk-dev +DEV_DOCKER_IMG:=concretefhe-dev +DEV_DOCKERFILE:=docker/Dockerfile.concretefhe-dev SRC_DIR:=concrete setup_env: diff --git a/docker/Dockerfile.hdk-dev b/docker/Dockerfile.concretefhe-dev similarity index 94% rename from docker/Dockerfile.hdk-dev rename to docker/Dockerfile.concretefhe-dev index bd5566383..fb49959d3 100644 --- a/docker/Dockerfile.hdk-dev +++ b/docker/Dockerfile.concretefhe-dev @@ -1,4 +1,4 @@ -FROM ghcr.io/zama-ai/hdk-env +FROM ghcr.io/zama-ai/concretefhe-env ENV SRC_DIR_NAME=src diff --git a/docker/Dockerfile.hdk-env b/docker/Dockerfile.concretefhe-env similarity index 100% rename from docker/Dockerfile.hdk-env rename to docker/Dockerfile.concretefhe-env From ff69f7424bb97d117ac57a5158ee834fa64a2086 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 7 Sep 2021 10:29:55 +0200 Subject: [PATCH 0192/1104] chore: rename secrets --- .github/workflows/continuous-integration.yaml | 8 ++++---- .github/workflows/docker-env.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 1f706025b..d53aa11a8 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -104,7 +104,7 @@ jobs: if: ${{ always() }} uses: rtCamp/action-slack-notify@v2 env: - SLACK_CHANNEL: hdk-updates + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: 'Build finished with status ${{ job.status }}' @@ -131,7 +131,7 @@ jobs: with: args: --delete env: - AWS_S3_BUCKET: ${{ secrets.AWS_HDK_DOCUMENTATION_BUCKET_NAME }} + AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} @@ -145,13 +145,13 @@ jobs: AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - DISTRIBUTION_ID: ${{ secrets.AWS_HDK_DOCUMENTATION_DISTRIBUTION_ID }} + DISTRIBUTION_ID: ${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }} - name: Slack Notification if: ${{ always() }} uses: rtCamp/action-slack-notify@v2 env: - SLACK_CHANNEL: hdk-updates + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: 'Publishing documentation finished with status ${{ job.status }}' diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index 016754607..a5a35c393 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -53,7 +53,7 @@ jobs: if: ${{ always() }} uses: rtCamp/action-slack-notify@v2 env: - SLACK_CHANNEL: hdk-updates + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: "Publishing Docker Image ${{ env.IMAGE_URL }} \ From 24c0735490f52bc86ac172fcfa6e6419f06001c1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 7 Sep 2021 17:01:47 +0200 Subject: [PATCH 0193/1104] fix(build): do not mark job as failing if slack notification fails --- .github/workflows/continuous-integration.yaml | 2 ++ .github/workflows/docker-env.yaml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index d53aa11a8..a9ffc5ada 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -102,6 +102,7 @@ jobs: recreate: true - name: Slack Notification if: ${{ always() }} + continue-on-error: true uses: rtCamp/action-slack-notify@v2 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} @@ -149,6 +150,7 @@ jobs: - name: Slack Notification if: ${{ always() }} + continue-on-error: true uses: rtCamp/action-slack-notify@v2 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index a5a35c393..ad82d803f 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -51,6 +51,7 @@ jobs: - name: Slack Notification if: ${{ always() }} + continue-on-error: true uses: rtCamp/action-slack-notify@v2 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} From 4778dc503b686397bccb5e411280d62f804a6dd0 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 6 Sep 2021 16:55:32 +0200 Subject: [PATCH 0194/1104] doc: use the imperative mode in docstring closes #230 --- Makefile | 2 +- concrete/common/compilation/artifacts.py | 12 +++++------ concrete/common/data_types/dtypes_helpers.py | 20 +++++++++---------- concrete/common/data_types/integers.py | 10 +++++----- concrete/common/mlir/converters.py | 20 +++++++++---------- concrete/common/operator_graph.py | 4 ++-- concrete/common/optimization/topological.py | 4 ++-- .../common/representation/intermediate.py | 16 +++++++-------- concrete/common/tracing/base_tracer.py | 6 +++--- concrete/common/tracing/tracing_helpers.py | 6 +++--- concrete/common/values/scalars.py | 4 ++-- concrete/common/values/tensors.py | 10 +++++----- concrete/numpy/compile.py | 4 ++-- concrete/numpy/np_dtypes_helpers.py | 8 ++++---- concrete/numpy/tracing.py | 8 ++++---- 15 files changed, 67 insertions(+), 67 deletions(-) diff --git a/Makefile b/Makefile index 288770470..2f59e6fd6 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ build_and_open_docs: clean_docs docs open_docs pydocstyle: @# From http://www.pydocstyle.org/en/stable/error_codes.html - poetry run pydocstyle $(SRC_DIR) --convention google --add-ignore=D1,D202 + poetry run pydocstyle $(SRC_DIR) --convention google --add-ignore=D1,D202 --add-select=D401 .PHONY: pydocstyle strip_nb: diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index 28b16d6fa..a0f5760f0 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -47,7 +47,7 @@ class CompilationArtifacts: self.mlir_of_the_final_operation_graph = None def add_function_to_compile(self, function: Union[Callable, str]): - """Adds the function to compile to artifacts. + """Add the function to compile to artifacts. Args: function (Union[Callable, str]): the function to compile or source code of it @@ -61,7 +61,7 @@ class CompilationArtifacts: ) def add_parameter_of_function_to_compile(self, name: str, value: Union[BaseValue, str]): - """Adds a parameter of the function to compile to the artifacts. + """Add a parameter of the function to compile to the artifacts. Args: name (str): name of the parameter @@ -74,7 +74,7 @@ class CompilationArtifacts: self.parameters_of_the_function_to_compile[name] = str(value) def add_operation_graph(self, name: str, operation_graph: OPGraph): - """Adds an operation graph to the artifacts. + """Add an operation graph to the artifacts. Args: name (str): name of the graph @@ -93,7 +93,7 @@ class CompilationArtifacts: self.final_operation_graph = operation_graph def add_final_operation_graph_bounds(self, bounds: Dict[ir.IntermediateNode, Dict[str, Any]]): - """Adds the bounds of the final operation graph to the artifacts. + """Add the bounds of the final operation graph to the artifacts. Args: bounds (Dict[ir.IntermediateNode, Dict[str, Any]]): the bound dictionary @@ -106,7 +106,7 @@ class CompilationArtifacts: self.bounds_of_the_final_operation_graph = bounds def add_final_operation_graph_mlir(self, mlir: str): - """Adds the mlir of the final operation graph to the artifacts. + """Add the mlir of the final operation graph to the artifacts. Args: mlir (str): the mlir code of the final operation graph @@ -119,7 +119,7 @@ class CompilationArtifacts: self.mlir_of_the_final_operation_graph = mlir def export(self): - """Exports the artifacts to a the output directory. + """Export the artifacts to a the output directory. Returns: None diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 0879521ee..7234c311f 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -23,7 +23,7 @@ BASE_DATA_TYPES = INTEGER_TYPES + FLOAT_TYPES def value_is_encrypted_scalar_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted ScalarValue of type Integer. + """Check that a value is an encrypted ScalarValue of type Integer. Args: value_to_check (BaseValue): The value to check @@ -35,7 +35,7 @@ def value_is_encrypted_scalar_integer(value_to_check: BaseValue) -> bool: def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted ScalarValue of type unsigned Integer. + """Check that a value is an encrypted ScalarValue of type unsigned Integer. Args: value_to_check (BaseValue): The value to check @@ -51,7 +51,7 @@ def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> boo def value_is_clear_scalar_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is a clear ScalarValue of type Integer. + """Check that a value is a clear ScalarValue of type Integer. Args: value_to_check (BaseValue): The value to check @@ -63,7 +63,7 @@ def value_is_clear_scalar_integer(value_to_check: BaseValue) -> bool: def value_is_scalar_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is a ScalarValue of type Integer. + """Check that a value is a ScalarValue of type Integer. Args: value_to_check (BaseValue): The value to check @@ -77,7 +77,7 @@ def value_is_scalar_integer(value_to_check: BaseValue) -> bool: def value_is_encrypted_tensor_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted TensorValue of type Integer. + """Check that a value is an encrypted TensorValue of type Integer. Args: value_to_check (BaseValue): The value to check @@ -89,7 +89,7 @@ def value_is_encrypted_tensor_integer(value_to_check: BaseValue) -> bool: def value_is_encrypted_tensor_unsigned_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is an encrypted TensorValue of type unsigned Integer. + """Check that a value is an encrypted TensorValue of type unsigned Integer. Args: value_to_check (BaseValue): The value to check @@ -105,7 +105,7 @@ def value_is_encrypted_tensor_unsigned_integer(value_to_check: BaseValue) -> boo def value_is_clear_tensor_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is a clear TensorValue of type Integer. + """Check that a value is a clear TensorValue of type Integer. Args: value_to_check (BaseValue): The value to check @@ -117,7 +117,7 @@ def value_is_clear_tensor_integer(value_to_check: BaseValue) -> bool: def value_is_tensor_integer(value_to_check: BaseValue) -> bool: - """Helper function to check that a value is a TensorValue of type Integer. + """Check that a value is a TensorValue of type Integer. Args: value_to_check (BaseValue): The value to check @@ -294,7 +294,7 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> def get_base_data_type_for_python_constant_data(constant_data: Union[int, float]) -> BaseDataType: - """Helper function to determine the BaseDataType to hold the input constant data. + """Determine the BaseDataType to hold the input constant data. Args: constant_data (Union[int, float]): The constant data for which to determine the @@ -320,7 +320,7 @@ def get_base_data_type_for_python_constant_data(constant_data: Union[int, float] def get_base_value_for_python_constant_data( constant_data: Union[int, float] ) -> Callable[..., ScalarValue]: - """Function to wrap the BaseDataType to hold the input constant data in a ScalarValue partial. + """Wrap the BaseDataType to hold the input constant data in a ScalarValue partial. The returned object can then be instantiated as an Encrypted or Clear version of the ScalarValue by calling it with the proper arguments forwarded to the ScalarValue `__init__` function diff --git a/concrete/common/data_types/integers.py b/concrete/common/data_types/integers.py index 7e5b0d79b..7ef0674d7 100644 --- a/concrete/common/data_types/integers.py +++ b/concrete/common/data_types/integers.py @@ -43,7 +43,7 @@ class Integer(base.BaseDataType): return 2 ** self.bit_width - 1 def can_represent_value(self, value_to_represent: int) -> bool: - """A helper function to check if a value is representable by the Integer. + """Check if a value is representable by the Integer. Args: value_to_represent (int): Value to check @@ -55,7 +55,7 @@ class Integer(base.BaseDataType): def create_signed_integer(bit_width: int) -> Integer: - """Convenience function to create a signed integer. + """Create a signed integer. Args: bit_width (int): width of the integer @@ -70,7 +70,7 @@ SignedInteger = create_signed_integer def create_unsigned_integer(bit_width: int) -> Integer: - """Convenience function to create an unsigned integer. + """Create an unsigned integer. Args: bit_width (int): width of the integer @@ -85,7 +85,7 @@ UnsignedInteger = create_unsigned_integer def make_integer_to_hold(values: Iterable[Any], force_signed: bool) -> Integer: - """Returns an Integer able to hold all values, it is possible to force the Integer to be signed. + """Return an Integer able to hold all values, it is possible to force the Integer to be signed. Args: values (Iterable[Any]): The values to hold @@ -108,7 +108,7 @@ def make_integer_to_hold(values: Iterable[Any], force_signed: bool) -> Integer: def get_bits_to_represent_value_as_integer(value: Any, force_signed: bool) -> int: - """Returns how many bits are required to represent a numerical Value. + """Return how many bits are required to represent a numerical Value. Args: value (Any): The value for which we want to know how many bits are required. diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index d5ab22fee..1af29dd75 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -25,7 +25,7 @@ from ..representation import intermediate as ir def add(node, preds, ir_to_mlir_node, ctx): - """Converter function for the addition intermediate node.""" + """Convert an addition intermediate node.""" assert len(node.inputs) == 2, "addition should have two inputs" assert len(node.outputs) == 1, "addition should have a single output" if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( @@ -47,7 +47,7 @@ def add(node, preds, ir_to_mlir_node, ctx): def _add_eint_int(node, preds, ir_to_mlir_node, ctx): - """Converter function for the addition intermediate node with operands (eint, int).""" + """Convert an addition intermediate node with (eint, int).""" lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.AddEintIntOp( @@ -58,7 +58,7 @@ def _add_eint_int(node, preds, ir_to_mlir_node, ctx): def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): - """Converter function for the addition intermediate node with operands (eint, int).""" + """Convert an addition intermediate node with (eint, int).""" lhs_node, rhs_node = preds lhs, rhs = lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.AddEintOp( @@ -69,7 +69,7 @@ def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): def sub(node, preds, ir_to_mlir_node, ctx): - """Converter function for the subtraction intermediate node.""" + """Convert a subtraction intermediate node.""" assert len(node.inputs) == 2, "subtraction should have two inputs" assert len(node.outputs) == 1, "subtraction should have a single output" if value_is_clear_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_unsigned_integer( @@ -82,7 +82,7 @@ def sub(node, preds, ir_to_mlir_node, ctx): def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): - """Converter function for the subtraction intermediate node with operands (int, eint).""" + """Convert a subtraction intermediate node with (int, eint).""" lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.SubIntEintOp( @@ -93,7 +93,7 @@ def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): def mul(node, preds, ir_to_mlir_node, ctx): - """Converter function for the multiplication intermediate node.""" + """Convert a multiplication intermediate node.""" assert len(node.inputs) == 2, "multiplication should have two inputs" assert len(node.outputs) == 1, "multiplication should have a single output" if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( @@ -111,7 +111,7 @@ def mul(node, preds, ir_to_mlir_node, ctx): def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): - """Converter function for the multiplication intermediate node with operands (eint, int).""" + """Convert a multiplication intermediate node with (eint, int).""" lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.MulEintIntOp( @@ -122,7 +122,7 @@ def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): def constant(node, _, __, ctx): - """Converter function for constant inputs.""" + """Convert a constant inputs.""" if not value_is_clear_scalar_integer(node.outputs[0]): raise TypeError("Don't support non-integer constants") dtype = cast(Integer, node.outputs[0].data_type) @@ -133,7 +133,7 @@ def constant(node, _, __, ctx): def apply_lut(node, preds, ir_to_mlir_node, ctx): - """Converter function for the arbitrary function intermediate node.""" + """Convert an arbitrary function intermediate node.""" assert len(node.inputs) == 1, "LUT should have a single input" assert len(node.outputs) == 1, "LUT should have a single output" if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): @@ -159,7 +159,7 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): def dot(node, preds, ir_to_mlir_node, ctx): - """Converter function for the dot intermediate node.""" + """Convert a dot intermediate node.""" assert len(node.inputs) == 2, "Dot should have two inputs" assert len(node.outputs) == 1, "Dot should have a single output" if not ( diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index d42a15419..313e25ec1 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -109,7 +109,7 @@ class OPGraph: return [self.output_nodes[idx] for idx in range(len(self.output_nodes))] def evaluate(self, inputs: Dict[int, Any]) -> Dict[ir.IntermediateNode, Any]: - """Function to evaluate a graph and get intermediate values for all nodes. + """Evaluate a graph and get intermediate values for all nodes. Args: inputs (Dict[int, Any]): The inputs to the program @@ -195,7 +195,7 @@ class OPGraph: succ.inputs[input_idx] = deepcopy(node.outputs[0]) def prune_nodes(self): - """Function to remove unreachable nodes from outputs.""" + """Remove unreachable nodes from outputs.""" current_nodes = set(self.output_nodes.values()) useful_nodes: Set[ir.IntermediateNode] = set() diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index cc66bb731..f80ec2161 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -15,7 +15,7 @@ def fuse_float_operations( op_graph: OPGraph, compilation_artifacts: Optional[CompilationArtifacts] = None, ): - """Finds and fuses float domains into single Integer to Integer ArbitraryFunction. + """Find and fuse float domains into single Integer to Integer ArbitraryFunction. Args: op_graph (OPGraph): The OPGraph to simplify @@ -90,7 +90,7 @@ def convert_float_subgraph_to_fused_node( terminal_node: ir.IntermediateNode, subgraph_all_nodes: Set[ir.IntermediateNode], ) -> Optional[Tuple[ir.ArbitraryFunction, ir.IntermediateNode]]: - """Converts a float subgraph to an equivalent fused ArbitraryFunction node. + """Convert a float subgraph to an equivalent fused ArbitraryFunction node. Args: op_graph (OPGraph): The OPGraph the float subgraph is part of. diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index 3bcf86507..5f4ba2d23 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -52,7 +52,7 @@ class IntermediateNode(ABC): @abstractmethod def evaluate(self, inputs: Dict[int, Any]) -> Any: - """Function to simulate what the represented computation would output for the given inputs. + """Simulate what the represented computation would output for the given inputs. Args: inputs (Dict[int, Any]): Dict containing the inputs for the evaluation @@ -63,7 +63,7 @@ class IntermediateNode(ABC): @classmethod def n_in(cls) -> int: - """Returns how many inputs the node has. + """Return how many inputs the node has. Returns: int: The number of inputs of the node. @@ -72,7 +72,7 @@ class IntermediateNode(ABC): @classmethod def requires_mix_values_func(cls) -> bool: - """Function to determine whether the Class requires a mix_values_func to be built. + """Determine whether the Class requires a mix_values_func to be built. Returns: bool: True if __init__ expects a mix_values_func argument. @@ -81,7 +81,7 @@ class IntermediateNode(ABC): @abstractmethod def label(self) -> str: - """Function to get the label of the node. + """Get the label of the node. Returns: str: the label of the node @@ -182,7 +182,7 @@ class Constant(IntermediateNode): @property def constant_data(self) -> Any: - """Returns the constant_data stored in the Constant node. + """Return the constant_data stored in the Constant node. Returns: Any: The constant data that was stored. @@ -230,7 +230,7 @@ class ArbitraryFunction(IntermediateNode): return self.op_name def get_table(self) -> List[Any]: - """Function to get the table for the current input value of this ArbitraryFunction. + """Get the table for the current input value of this ArbitraryFunction. Returns: List[Any]: The table. @@ -255,7 +255,7 @@ class ArbitraryFunction(IntermediateNode): def default_dot_evaluation_function(lhs: Any, rhs: Any) -> Any: - """Default python dot implementation for 1D iterable arrays. + """Return the default python dot implementation for 1D iterable arrays. Args: lhs (Any): lhs vector of the dot. @@ -268,7 +268,7 @@ def default_dot_evaluation_function(lhs: Any, rhs: Any) -> Any: class Dot(IntermediateNode): - """Node representing a dot product.""" + """Return the node representing a dot product.""" _n_in: int = 2 # Optional, same issue as in ArbitraryFunction for mypy diff --git a/concrete/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py index 851362802..dfaa12548 100644 --- a/concrete/common/tracing/base_tracer.py +++ b/concrete/common/tracing/base_tracer.py @@ -28,7 +28,7 @@ class BaseTracer(ABC): @abstractmethod def _supports_other_operand(self, other: Any) -> bool: - """Function to check if the current class supports tracing with the other operand. + """Check if the current class supports tracing with the other operand. Args: other (Any): the operand to check compatibility with. @@ -40,7 +40,7 @@ class BaseTracer(ABC): @abstractmethod def _make_const_input_tracer(self, constant_data: Any) -> "BaseTracer": - """Helper function to create a tracer for a constant input. + """Create a tracer for a constant input. Args: constant_data (Any): The constant to store. @@ -63,7 +63,7 @@ class BaseTracer(ABC): inputs: Iterable[Union["BaseTracer", Any]], computation_to_trace: Type[ir.IntermediateNode], ) -> Tuple["BaseTracer", ...]: - """Helper functions to instantiate all output BaseTracer for a given computation. + """Instantiate all output BaseTracer for a given computation. Args: inputs (Iterable[Union[BaseTracer, Any]]): Previous BaseTracer or data used as inputs diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index 34043bda7..8e0824c47 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -15,7 +15,7 @@ def make_input_tracers( tracer_class: Type[BaseTracer], function_parameters: OrderedDict[str, BaseValue], ) -> OrderedDict[str, BaseTracer]: - """Helper function to create tracers for a function's parameters. + """Create tracers for a function's parameters. Args: tracer_class (Type[BaseTracer]): the class of tracer to create an Input for @@ -37,7 +37,7 @@ def make_input_tracer( input_idx: int, input_value: BaseValue, ) -> BaseTracer: - """Helper function to create a tracer for an input value. + """Create a tracer for an input value. Args: tracer_class (Type[BaseTracer]): the class of tracer to create an Input for @@ -55,7 +55,7 @@ def make_input_tracer( def prepare_function_parameters( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] ) -> OrderedDict[str, BaseValue]: - """Function to filter the passed function_parameters to trace function_to_trace. + """Filter the passed function_parameters to trace function_to_trace. Args: function_to_trace (Callable): function that will be traced for which parameters are checked diff --git a/concrete/common/values/scalars.py b/concrete/common/values/scalars.py index 38a9a87e4..1b057fa99 100644 --- a/concrete/common/values/scalars.py +++ b/concrete/common/values/scalars.py @@ -16,7 +16,7 @@ class ScalarValue(BaseValue): def make_clear_scalar(data_type: BaseDataType) -> ScalarValue: - """Helper to create a clear ScalarValue. + """Create a clear ScalarValue. Args: data_type (BaseDataType): The data type for the value. @@ -28,7 +28,7 @@ def make_clear_scalar(data_type: BaseDataType) -> ScalarValue: def make_encrypted_scalar(data_type: BaseDataType) -> ScalarValue: - """Helper to create an encrypted ScalarValue. + """Create an encrypted ScalarValue. Args: data_type (BaseDataType): The data type for the value. diff --git a/concrete/common/values/tensors.py b/concrete/common/values/tensors.py index 966f7c8e7..dc3d421d0 100644 --- a/concrete/common/values/tensors.py +++ b/concrete/common/values/tensors.py @@ -41,7 +41,7 @@ class TensorValue(BaseValue): @property def shape(self) -> Tuple[int, ...]: - """The TensorValue shape property. + """Return the TensorValue shape property. Returns: Tuple[int, ...]: The TensorValue shape. @@ -50,7 +50,7 @@ class TensorValue(BaseValue): @property def ndim(self) -> int: - """The TensorValue ndim property. + """Return the TensorValue ndim property. Returns: int: The TensorValue ndim. @@ -59,7 +59,7 @@ class TensorValue(BaseValue): @property def size(self) -> int: - """The TensorValue size property. + """Return the TensorValue size property. Returns: int: The TensorValue size. @@ -71,7 +71,7 @@ def make_clear_tensor( data_type: BaseDataType, shape: Optional[Tuple[int, ...]] = None, ) -> TensorValue: - """Helper to create a clear TensorValue. + """Create a clear TensorValue. Args: data_type (BaseDataType): The data type for the tensor. @@ -87,7 +87,7 @@ def make_encrypted_tensor( data_type: BaseDataType, shape: Optional[Tuple[int, ...]] = None, ) -> TensorValue: - """Helper to create an encrypted TensorValue. + """Create an encrypted TensorValue. Args: data_type (BaseDataType): The data type for the tensor. diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index f3700a1fc..e3de2a337 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -199,7 +199,7 @@ def _compile_numpy_function_internal( compilation_artifacts: CompilationArtifacts, show_mlir: bool, ) -> CompilerEngine: - """Internal part of the API to be able to compile an homomorphic program. + """Compile an homomorphic program (internal part of the API). Args: function_to_compile (Callable): The function you want to compile @@ -254,7 +254,7 @@ def compile_numpy_function( compilation_artifacts: Optional[CompilationArtifacts] = None, show_mlir: bool = False, ) -> CompilerEngine: - """Main API to be able to compile an homomorphic program. + """Compile an homomorphic program (main API). Args: function_to_compile (Callable): The function to compile diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index cbba8f9df..69586c1cc 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -33,7 +33,7 @@ SUPPORTED_DTYPE_MSG_STRING = ", ".join(sorted(str(dtype) for dtype in SUPPORTED_ def convert_numpy_dtype_to_base_data_type(numpy_dtype: DTypeLike) -> BaseDataType: - """Helper function to get the corresponding BaseDataType from a numpy dtype. + """Get the corresponding BaseDataType from a numpy dtype. Args: numpy_dtype (DTypeLike): Any python object that can be translated to a numpy.dtype @@ -99,7 +99,7 @@ def convert_base_data_type_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.d def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> BaseDataType: - """Helper function to determine the BaseDataType to hold the input constant data. + """Determine the BaseDataType to hold the input constant data. Args: constant_data (Any): The constant data for which to determine the @@ -124,7 +124,7 @@ def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> def get_base_value_for_numpy_or_python_constant_data( constant_data: Any, ) -> Callable[..., BaseValue]: - """Helper function to determine the BaseValue and BaseDataType to hold the input constant data. + """Determine the BaseValue and BaseDataType to hold the input constant data. This function is able to handle numpy types @@ -158,7 +158,7 @@ def get_numpy_function_output_dtype( function: Union[numpy.ufunc, Callable], input_dtypes: List[BaseDataType], ) -> List[numpy.dtype]: - """Function to record the output dtype of a numpy function given some input types. + """Record the output dtype of a numpy function given some input types. Args: function (Union[numpy.ufunc, Callable]): The numpy function whose output types need to diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index d5247dc38..0c1739852 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -134,7 +134,7 @@ class NPTracer(BaseTracer): def _unary_operator( cls, unary_operator, unary_operator_string, *input_tracers: "NPTracer", **kwargs ) -> "NPTracer": - """Function to trace an unary operator. + """Trace an unary operator. Returns: NPTracer: The output NPTracer containing the traced function @@ -158,7 +158,7 @@ class NPTracer(BaseTracer): return output_tracer def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": - """Function to trace numpy.dot. + """Trace numpy.dot. Returns: NPTracer: The output NPTracer containing the traced function @@ -285,7 +285,7 @@ class NPTracer(BaseTracer): def _get_fun(function: numpy.ufunc): - """Helper function to wrap _unary_operator in a lambda to populate NPTRACER.UFUNC_ROUTING.""" + """Wrap _unary_operator in a lambda to populate NPTRACER.UFUNC_ROUTING.""" # We have to access this method to be able to build NPTracer.UFUNC_ROUTING # dynamically @@ -303,7 +303,7 @@ NPTracer.UFUNC_ROUTING = {fun: _get_fun(fun) for fun in NPTracer.LIST_OF_SUPPORT def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] ) -> OPGraph: - """Function used to trace a numpy function. + """Trace a numpy function. Args: function_to_trace (Callable): The function you want to trace From 567a382632bf20c18332862346f5b11ba795ca2f Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 6 Sep 2021 11:12:39 +0200 Subject: [PATCH 0195/1104] doc: let's reorganise markdowns and documentation closes #291 --- README.md | 23 ++- docs/dev/CONTRIBUTING.md | 102 ++++++++++ docs/dev/DOCUMENTING.md | 25 +++ docs/dev/GETTING-STARTED.md | 261 -------------------------- docs/dev/TERMINOLOGY_AND_STRUCTURE.md | 30 +++ docs/index.rst | 19 +- docs/install/DOCKER.md | 49 +++++ docs/install/INSTALLING.md | 87 +++++++++ docs/user/FIRST_USE.md | 3 + 9 files changed, 335 insertions(+), 264 deletions(-) create mode 100644 docs/dev/CONTRIBUTING.md create mode 100644 docs/dev/DOCUMENTING.md delete mode 100644 docs/dev/GETTING-STARTED.md create mode 100644 docs/dev/TERMINOLOGY_AND_STRUCTURE.md create mode 100644 docs/install/DOCKER.md create mode 100644 docs/install/INSTALLING.md create mode 100644 docs/user/FIRST_USE.md diff --git a/README.md b/README.md index 7e4afbdba..b31760944 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,25 @@ Concrete Framework Python API - collection of tools to FHE all the things -Developers can take a look at [GETTING-STARTED.md](docs/dev/GETTING-STARTED.md) to get all the relevant informations on the project. +## Installing + +Installation steps are described in [INSTALLING.md](docs/install/INSTALLING.md). + +## Using Docker + +Information about how to use Docker are available in [DOCKER.md](docs/install/DOCKER.md). + +## Documenting + +Some information about how to build the documentation of `concretefhe` are available in [DOCUMENTING.md](docs/dev/DOCUMENTING.md). Notably, our documentation is pushed to [https://hdk.zama.ai](https://hdk.zama.ai). + +## Developping + +Some information about our terminology and the infrastructure of `concretefhe` are available in [TERMINOLOGY_AND_STRUCTURE.md](docs/dev/TERMINOLOGY_AND_STRUCTURE.md). An in-depth look at what is done in `concretefhe` is available in [COMPILATION.md](docs/dev/COMPILATION.md). + +## Contributing + +Information about how to contribute are available in [CONTRIBUTING.md](docs/dev/CONTRIBUTING.md). + + + diff --git a/docs/dev/CONTRIBUTING.md b/docs/dev/CONTRIBUTING.md new file mode 100644 index 000000000..713d73031 --- /dev/null +++ b/docs/dev/CONTRIBUTING.md @@ -0,0 +1,102 @@ + +# Contributing + +There are two ways to contribute to `concretefhe`: +- you can open issues to report bugs, typos and suggest ideas +- you can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests, so please make sure to get in touch before you do! + +Let's go over some other important things that you need to be careful about. + +## Creating a new branch + +We are using a consistent branch naming scheme, and you are expected to follow it as well. Here is the format and some examples. + +```shell +git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description_$issue_id +``` + +e.g. + +```shell +git checkout -b feat/explicit-tlu_11 +git checkout -b fix/tracing_indexing_42 +``` + +## Before committing + +### Conformance + +Each commit to `concretefhe` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. + +```shell +make pcc +make pytest +``` + +### pytest + +Of course, tests must be passing as well. + +```shell +make pytest +``` + +### Coverage + +The last requirement is to make sure you get a hundred percent code coverage. You can verify this using the following command (after having done `make pytest`). + +```shell +make coverage +``` + +Remark that only calling `make pytest` will give you information about the coverage, at the end of the execution, but the test will not return a failure if the coverage is not a hundred percent, as opposed to a call to `make coverage`. + +Note that this will compare the coverage with `origin/main`. If you want to set a custom base branch, you can specify `BB` environment variable like so `BB=$YOUR_BASE_BRANCH make coverage`. + +If your coverage is below hundred percent, you should write more tests and then create the pull request. If you ignore this warning and create the PR, GitHub actions will fail and your PR will not be merged anyway. + +## Commiting + +We are using a consistent commit naming scheme, and you are expected to follow it as well. Here is the format and some examples. + +```shell +git commit -m "{feat|fix|refactor|test|benchmark|doc|style|chore}{($location)}?: description of the change" +``` + +e.g. + +```shell +git commit -m "feat: implement bounds checking" +git commit -m "feat(debugging): add an helper function to draw intermediate representation" +git commit -m "fix(tracing): fix a bug that crashed pytorch tracer" +``` + +To learn more about conventional commits, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page. + +## Before creating pull request + +We remind that only official contributors can send pull requests. To become such an official contributor, please email hello@zama.ai. + +You should rebase on top of `main` branch before you create your pull request. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. + +```shell +# fetch the list of active remote branches +git fetch --all --prune + +# checkout to main +git checkout main + +# pull the latest changes to main (--ff-only is there to prevent accidental commits to main) +git pull --ff-only + +# checkout back to your branch +git checkout $YOUR_BRANCH + +# rebase on top of main branch +git rebase main + +# push the latest version of the local branch to remote +git push --force +``` + +You can learn more about rebasing in [here](https://git-scm.com/docs/git-rebase). diff --git a/docs/dev/DOCUMENTING.md b/docs/dev/DOCUMENTING.md new file mode 100644 index 000000000..b208ac272 --- /dev/null +++ b/docs/dev/DOCUMENTING.md @@ -0,0 +1,25 @@ + +# Documenting + +## Making docs with Sphinx + +One can simply create docs with Sphinx and open them, by doing: + +```shell +make docs +``` + +The documentation contains both files written by hand by developpers and files automatically created by parsing the source files. + +### Opening doc + +On macOS, one can do + +```shell +make open_docs +``` + +On other systems, simply open `docs/_build/html/index.html` + + + diff --git a/docs/dev/GETTING-STARTED.md b/docs/dev/GETTING-STARTED.md deleted file mode 100644 index f017f9e54..000000000 --- a/docs/dev/GETTING-STARTED.md +++ /dev/null @@ -1,261 +0,0 @@ -# Getting Started - -## Preparation - -Before you can start improving `concretefhe` you need to set up your development environment! This section will show how you can do that. - -### Installing Python v3.8 - -`concretefhe` is a `Python` library. So `Python` should be installed to develop `concretefhe`. `v3.8` is recommended because our CI also uses `v3.8`. - -You can follow [this](https://realpython.com/installing-python/) guide to install it (alternatively you can google `how to install python 3.8`). - -### Installing Poetry - -`Poetry` is our package manager. It simplifies dependency and environment management by a lot. - -You can follow [this](https://python-poetry.org/docs/#installation) official guide to install it. - -### Installing make - -The dev tools use make to launch the various commands. - -On Linux you can install make from your distribution's preferred package manager. - -On Mac OS you can install a more recent version of make via brew: - -```bash -# check for gmake -which gmake -# If you don't have it, it will error out, install gmake -brew install make -# recheck, now you should have gmake -which gmake -``` - -It is possible to install gmake as make, check this [StackOverflow post](https://stackoverflow.com/questions/38901894/how-can-i-install-a-newer-version-of-make-on-mac-os) for more infos. - -On Windows check [this GitHub gist](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make). - -**/!\\ In the next sections, be sure to use the proper `make` tool for your system, `make`, `gmake` or other. /!\\** - -### Cloning repository - -Now, it's time to get the source code of `concretefhe`. You can use the following command to do that. - -```shell -git clone https://github.com/zama-ai/concretefhe-internal.git -``` - -### Setting up environment - -We are going to make use of virtual environments. This helps to keep the project isolated from other `Python` projects in the system. The following commands will create a new virtual environment under the project directory and install dependencies to it. - -```shell -cd concrete -make setup_env -``` - -### Activating the environment - -Finally, all we need to do is to activate the newly created environment using the following command. - -```shell -source .venv/bin/activate -``` - -### Leaving the environment - -After your work is done you can simply run the following command to leave the environment. - -```shell -deactivate -``` - -### Syncing environment with the latest changes - -From time to time, new dependencies will be added to project or the old ones will be removed. The command below will make sure the project have proper environment. So run it regularly! - -```shell -make sync_env -``` - -## Terminology - -In this section we will go over some terms that we use throughout the project. - -- intermediate representation - - a data structure to represent a calculation - - basically a computation graph where nodes are either inputs or operations on other nodes -- tracing - - it is our technique to take directly a plain numpy function from a user and deduce its intermediate representation in a painless way for the user -- bounds - - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) - - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node - -## Module Structure - -In this section, we will discuss the module structure of concretefhe briefly. You are encouraged to check individual `.py` files to learn more! - -- concrete - - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) - - bounds_measurement: utilities for determining bounds of intermediate representation - - compilation: type definitions related to compilation (e.g., compilation config, compilation artifacts) - - data_types: type definitions of typing information of intermediate representation - - debugging: utilities for printing/displaying intermediate representation - - extensions: utilities that provide special functionality to our users - - representation: type definitions of intermediate representation - - tracing: utilities for generic function tracing used during intermediate representation creation - - numpy: numpy frontend of concrete - -## Working in Docker - -### Setting up docker and X forwarding - -Before you start this section, go ahead and install docker. You can follow [this](https://docs.docker.com/engine/install/) official guide for that. - -#### Linux - -```console -xhost +localhost -``` - -#### Mac OS - -To be able to use X forwarding on Mac OS: first, you need to install xquartz. Secondly, open XQuartz.app application, and open a new terminal within XQuartz.app. Make sure in the application parameters to authorize network connections are set (currently in the Security settings); finally, in the XQuartz.app terminal, type - -```console -xhost +127.0.0.1 -``` - -and now, the X server should be all set in docker (in the regular terminal). - -#### Windows - -Install Xming and use Xlaunch: -- Multiple Windows, Display number: 0 -- Start no client -- **IMPORTANT**: Check `No Access Control` -- You can save this configuration to re-launch easily, then click finish. - -### Logging in and building the image - -Docker image of `concretefhe` is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. - -Upon joining to the team, you need to log in using the following command: - -```shell -docker login ghcr.io -``` - -This command will ask for a username and a password. For username, just enter your GitHub username. For password, you should create a personal access token from [here](https://github.com/settings/tokens) selecting `read:packages` permission. Just paste the generated access token as your password, and you are good to go. - -Once you do that you can get inside the docker environment using the following command: - -```shell -make docker_build_and_start -``` - -After you finish your work, you can leave the docker by using the `exit` command or by pressing `CTRL + D`. - -## Contributing - -Now, you have a working environment, and you know what is where in the project. - -There are two ways to contribute to `concretefhe`: -- you can open issues to report bugs, typos and suggest ideas -- you can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests, so please make sure to get in touch before you do! - -Let's go over some other important things that you need to be careful about. - -### Creating a new branch - -We are using a consistent branch naming scheme, and you are expected to follow it as well. Here is the format and some examples. - -```shell -git checkout -b {feat|fix|refactor|test|benchmark|doc|style|chore}/short-description_$issue_id -``` - -e.g. - -```shell -git checkout -b feat/explicit-tlu_11 -git checkout -b fix/tracing_indexing_42 -``` - -### Before committing - -Each commit to `concretefhe` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. - -```shell -make -k pcc -make pytest -``` - -### Commiting - -We are using a consistent commit naming scheme, and you are expected to follow it as well. Here is the format and some examples. - -```shell -git commit -m "{feat|fix|refactor|test|benchmark|doc|style|chore}{($location)}?: description of the change" -``` - -e.g. - -```shell -git commit -m "feat: implement bounds checking" -git commit -m "feat(debugging): add an helper function to draw intermediate representation" -git commit -m "fix(tracing): fix a bug that crashed pytorch tracer" -``` - -To learn more about conventional commits, check [this](https://www.conventionalcommits.org/en/v1.0.0/) page. - -### Before creating pull request - -We remind that only official contributors can send pull requests. To become such an official contributor, please email hello@zama.ai. - -You should rebase on top of `main` branch before you create your pull request. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. - -```shell -# fetch the list of active remote branches -git fetch --all --prune - -# checkout to main -git checkout main - -# pull the latest changes to main (--ff-only is there to prevent accidental commits to main) -git pull --ff-only - -# checkout back to your branch -git checkout $YOUR_BRANCH - -# rebase on top of main branch -git rebase main - -# push the latest version of the local branch to remote -git push --force -``` - -You can learn more about rebasing in [here](https://git-scm.com/docs/git-rebase). - -The last requirement before creating your PR is to make sure you get a hundred percent code coverage. You can verify this using the following command. - -```shell -make pytest -make coverage -``` - -Note that this will compare the coverage with `origin/main`. If you want to set a custom base branch, you can specify `BB` environment variable like so `BB=$YOUR_BASE_BRANCH make coverage`. - -If your coverage is below hundred percent, you should write more tests and then create the pull request. If you ignore this warning and create the PR, GitHub actions will fail and your PR will not be merged anyway. - -### Making docs with Sphinx - -One can simply create docs with Sphinx and open them, by doing: - -```shell -make build_and_open_docs -``` - -The documentation contains both files written by hand by developpers and files automatically created by parsing the source files. - diff --git a/docs/dev/TERMINOLOGY_AND_STRUCTURE.md b/docs/dev/TERMINOLOGY_AND_STRUCTURE.md new file mode 100644 index 000000000..d273f2598 --- /dev/null +++ b/docs/dev/TERMINOLOGY_AND_STRUCTURE.md @@ -0,0 +1,30 @@ +# Terminology and Structure + +## Terminology + +In this section we will go over some terms that we use throughout the project. + +- intermediate representation + - a data structure to represent a calculation + - basically a computation graph where nodes are either inputs or operations on other nodes +- tracing + - it is our technique to take directly a plain numpy function from a user and deduce its intermediate representation in a painless way for the user +- bounds + - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) + - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node + +## Module Structure + +In this section, we will discuss the module structure of `concretefhe` briefly. You are encouraged to check individual `.py` files to learn more! + +- concrete + - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) + - bounds_measurement: utilities for determining bounds of intermediate representation + - compilation: type definitions related to compilation (e.g., compilation config, compilation artifacts) + - data_types: type definitions of typing information of intermediate representation + - debugging: utilities for printing/displaying intermediate representation + - extensions: utilities that provide special functionality to our users + - representation: type definitions of intermediate representation + - tracing: utilities for generic function tracing used during intermediate representation creation + - numpy: numpy frontend of the package + diff --git a/docs/index.rst b/docs/index.rst index dd84fd8f5..7e5a8f13f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,11 +4,26 @@ Homomorphic Development Kit's documentation .. toctree:: :maxdepth: 2 - :caption: Developer docs + :caption: Installing - dev/GETTING-STARTED.md + install/INSTALLING.md + install/DOCKER.md + +.. toctree:: + :maxdepth: 2 + :caption: User doc + + user/FIRST_USE.md + +.. toctree:: + :maxdepth: 2 + :caption: Developer doc + + dev/TERMINOLOGY_AND_STRUCTURE.md dev/COMPILATION.md dev/FLOAT-FUSING.md + dev/DOCUMENTING.md + dev/CONTRIBUTING.md .. toctree:: :maxdepth: 5 diff --git a/docs/install/DOCKER.md b/docs/install/DOCKER.md new file mode 100644 index 000000000..5c64a8a1c --- /dev/null +++ b/docs/install/DOCKER.md @@ -0,0 +1,49 @@ +# Docker + +## Setting up docker and X forwarding + +Before you start this section, go ahead and install docker. You can follow [this](https://docs.docker.com/engine/install/) official guide for that. + +### Linux + +```console +xhost +localhost +``` + +### Mac OS + +To be able to use X forwarding on Mac OS: first, you need to install xquartz. Secondly, open XQuartz.app application, and open a new terminal within XQuartz.app. Make sure in the application parameters to authorize network connections are set (currently in the Security settings); finally, in the XQuartz.app terminal, type + +```console +xhost +127.0.0.1 +``` + +and now, the X server should be all set in docker (in the regular terminal). + +### Windows + +Install Xming and use Xlaunch: +- Multiple Windows, Display number: 0 +- Start no client +- **IMPORTANT**: Check `No Access Control` +- You can save this configuration to re-launch easily, then click finish. + +## Logging in and building the image + +Docker image of `concretefhe` is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. + +Upon joining to the team, you need to log in using the following command: + +```shell +docker login ghcr.io +``` + +This command will ask for a username and a password. For username, just enter your GitHub username. For password, you should create a personal access token from [here](https://github.com/settings/tokens) selecting `read:packages` permission. Just paste the generated access token as your password, and you are good to go. + +Once you do that you can get inside the docker environment using the following command: + +```shell +make docker_build_and_start +``` + +After you finish your work, you can leave the docker by using the `exit` command or by pressing `CTRL + D`. diff --git a/docs/install/INSTALLING.md b/docs/install/INSTALLING.md new file mode 100644 index 000000000..a0408c683 --- /dev/null +++ b/docs/install/INSTALLING.md @@ -0,0 +1,87 @@ + +# Installing + +## Installing Python v3.8 + +`concretefhe` is a `Python` library. So `Python` should be installed to develop `concretefhe`. `v3.8` is the only supported version. + +You can follow [this](https://realpython.com/installing-python/) guide to install it (alternatively you can google `how to install python 3.8`). + +## Installing Poetry + +`Poetry` is our package manager. It simplifies dependency and environment management by a lot. + +You can follow [this](https://python-poetry.org/docs/#installation) official guide to install it. + +## Installing make + +The dev tools use make to launch the various commands. + +On Linux you can install make from your distribution's preferred package manager. + +On Mac OS you can install a more recent version of make via brew: + +```bash +# check for gmake +which gmake +# If you don't have it, it will error out, install gmake +brew install make +# recheck, now you should have gmake +which gmake +``` + +It is possible to install gmake as make, check this [StackOverflow post](https://stackoverflow.com/questions/38901894/how-can-i-install-a-newer-version-of-make-on-mac-os) for more infos. + +On Windows check [this GitHub gist](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make). + +**/!\\ In the next sections, be sure to use the proper `make` tool for your system, `make`, `gmake` or other. /!\\** + +## Cloning repository + +Now, it's time to get the source code of `concretefhe`. You can use the following command to do that. + +```shell +git clone https://github.com/zama-ai/concretefhe-internal.git +``` + +## Setting up environment + +We are going to make use of virtual environments. This helps to keep the project isolated from other `Python` projects in the system. The following commands will create a new virtual environment under the project directory and install dependencies to it. + +```shell +cd concretefhe-internal +make setup_env +``` + +## Activating the environment + +Finally, all we need to do is to activate the newly created environment using the following command. + +### macOS or Linux + +```shell +source .venv/bin/activate +``` + +### Windows + +```shell +source .venv/Scripts/activate +``` + +## Leaving the environment + +After your work is done you can simply run the following command to leave the environment. + +```shell +deactivate +``` + +## Syncing environment with the latest changes + +From time to time, new dependencies will be added to project or the old ones will be removed. The command below will make sure the project have proper environment. So run it regularly! + +```shell +make sync_env +``` + diff --git a/docs/user/FIRST_USE.md b/docs/user/FIRST_USE.md new file mode 100644 index 000000000..186e1f055 --- /dev/null +++ b/docs/user/FIRST_USE.md @@ -0,0 +1,3 @@ +# First Use + +To be continued \ No newline at end of file From 231a1461714a68a8b93a2e51b27502c4f98058ba Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 3 Sep 2021 10:38:23 +0300 Subject: [PATCH 0196/1104] chore: remove pytest-benchmark as a new benchmark architecture is on the way --- benchmarks/test_compilation_and_evaluation.py | 84 ---- poetry.lock | 373 +++++++++--------- pyproject.toml | 1 - 3 files changed, 181 insertions(+), 277 deletions(-) delete mode 100644 benchmarks/test_compilation_and_evaluation.py diff --git a/benchmarks/test_compilation_and_evaluation.py b/benchmarks/test_compilation_and_evaluation.py deleted file mode 100644 index c1567ac21..000000000 --- a/benchmarks/test_compilation_and_evaluation.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Benchmark module for the entire compilation pipeline""" - -import itertools - -import pytest - -import concrete.numpy as hnp - - -@pytest.mark.parametrize( - "function,parameters,ranges", - [ - pytest.param( - lambda x: x + 42, - {"x": hnp.EncryptedScalar(hnp.SignedInteger(4))}, - ((-2, 2),), - id="x + 42", - ), - pytest.param( - lambda x, y: x + y, - { - "x": hnp.EncryptedScalar(hnp.SignedInteger(4)), - "y": hnp.EncryptedScalar(hnp.UnsignedInteger(4)), - }, - ((-2, 2), (20, 30)), - id="x + y", - ), - ], -) -def test_compilation(benchmark, function, parameters, ranges): - """Benchmark function for compilation of various functions""" - - def dataset(args): - for prod in itertools.product(*args): - yield prod - - @benchmark - def compilation(): - hnp.compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) - - -@pytest.mark.parametrize( - "function,parameters,ranges,inputs", - [ - pytest.param( - lambda x: x + 420, - {"x": hnp.EncryptedScalar(hnp.SignedInteger(4))}, - ((-2, 2),), - [ - {0: -2}, - {0: 0}, - {0: 1}, - ], - id="x + 420", - ), - pytest.param( - lambda x, y: x + y, - { - "x": hnp.EncryptedScalar(hnp.SignedInteger(4)), - "y": hnp.EncryptedScalar(hnp.UnsignedInteger(4)), - }, - ((-2, 2), (20, 30)), - [ - {0: -2, 1: 25}, - {0: 0, 1: 30}, - {0: 1, 1: 22}, - ], - id="x + y", - ), - ], -) -def test_evaluation(benchmark, function, parameters, ranges, inputs): - """Benchmark function for evaluation of various functions""" - - def dataset(args): - for prod in itertools.product(*args): - yield prod - - graph = hnp.compile_numpy_function_into_op_graph(function, parameters, dataset(ranges)) - - @benchmark - def evaluation(): - for x in inputs: - graph.evaluate(x) diff --git a/poetry.lock b/poetry.lock index 681ef239b..f9d336b67 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,24 +24,23 @@ python-versions = "*" [[package]] name = "argon2-cffi" -version = "20.1.0" +version = "21.1.0" description = "The secure Argon2 password hashing algorithm." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [package.dependencies] cffi = ">=1.0.0" -six = "*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "wheel", "pre-commit"] -docs = ["sphinx"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest", "sphinx", "furo", "wheel", "pre-commit"] +docs = ["sphinx", "furo"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] [[package]] name = "astroid" -version = "2.6.6" +version = "2.7.3" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -120,7 +119,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "4.0.0" +version = "4.1.0" description = "An easy safelist-based HTML-sanitizing tool." category = "dev" optional = false @@ -229,21 +228,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.3.3" -description = "Run coverage and linting reports on diffs" +version = "6.2.1" +description = "Automatically find diff lines that need test coverage." category = "dev" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">= 3.6" [package.dependencies] chardet = ">=3.0.0" Jinja2 = ">=2.7.1" -jinja2_pluralize = ">=0.3.0,<0.4.0" -pluggy = ">=0.13.1,<0.14.0" -Pygments = ">=2.9.0,<3.0.0" - -[package.extras] -toml = ["tomli (>=1.2.1,<2.0.0)"] +jinja2-pluralize = "*" +pluggy = "*" +pygments = "*" [[package]] name = "docutils" @@ -308,7 +304,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.4" +version = "4.8.1" description = "Read metadata from Python packages" category = "dev" optional = false @@ -363,7 +359,7 @@ test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "jedi (<=0.17.2)"] [[package]] name = "ipython" -version = "7.26.0" +version = "7.27.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -403,7 +399,7 @@ python-versions = "*" [[package]] name = "ipywidgets" -version = "7.6.3" +version = "7.6.4" description = "IPython HTML widgets for Jupyter" category = "dev" optional = false @@ -412,6 +408,7 @@ python-versions = "*" [package.dependencies] ipykernel = ">=4.5.1" ipython = {version = ">=4.0.0", markers = "python_version >= \"3.3\""} +ipython-genutils = ">=0.2.0,<0.3.0" jupyterlab-widgets = {version = ">=1.0.0", markers = "python_version >= \"3.6\""} nbformat = ">=4.2.0" traitlets = ">=4.3.1" @@ -511,7 +508,7 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "7.0.1" +version = "7.0.2" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false @@ -573,7 +570,7 @@ pygments = ">=2.4.1,<3" [[package]] name = "jupyterlab-widgets" -version = "1.0.0" +version = "1.0.1" description = "A JupyterLab extension." category = "dev" optional = false @@ -581,11 +578,11 @@ python-versions = ">=3.6" [[package]] name = "kiwisolver" -version = "1.3.1" +version = "1.3.2" description = "A fast implementation of the Cassowary constraint solver" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "lazy-object-proxy" @@ -710,7 +707,7 @@ python-versions = "*" [[package]] name = "myst-parser" -version = "0.15.1" +version = "0.15.2" description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." category = "dev" optional = false @@ -722,7 +719,7 @@ jinja2 = "*" markdown-it-py = ">=1.0.0,<2.0.0" mdit-py-plugins = ">=0.2.8,<0.3.0" pyyaml = "*" -sphinx = ">=3,<5" +sphinx = ">=3.1,<5" [package.extras] code_style = ["pre-commit (>=2.12,<3.0)"] @@ -948,19 +945,32 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "platformdirs" +version = "2.3.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" @@ -1000,14 +1010,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "py-cpuinfo" -version = "8.0.0" -description = "Get CPU info with pure Python 2 & 3" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pycodestyle" version = "2.7.0" @@ -1079,17 +1081,18 @@ python-versions = ">=3.7" [[package]] name = "pylint" -version = "2.9.6" +version = "2.10.2" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.6.5,<2.7" +astroid = ">=2.7.2,<2.8" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" toml = ">=0.7.1" [[package]] @@ -1110,7 +1113,7 @@ python-versions = ">=3.6" [[package]] name = "pytest" -version = "6.2.4" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -1123,30 +1126,13 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] -[[package]] -name = "pytest-benchmark" -version = "3.4.1" -description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -py-cpuinfo = "*" -pytest = ">=3.8" - -[package.extras] -aspect = ["aspectlib"] -elasticsearch = ["elasticsearch"] -histogram = ["pygal", "pygaljs"] - [[package]] name = "pytest-cov" version = "2.12.1" @@ -1192,7 +1178,7 @@ python-versions = "*" [[package]] name = "pywinpty" -version = "1.1.3" +version = "1.1.4" description = "Pseudo terminal support for Windows from Python." category = "dev" optional = false @@ -1250,7 +1236,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" [[package]] name = "regex" -version = "2021.8.3" +version = "2021.8.28" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -1473,15 +1459,12 @@ python-versions = ">= 3.5" [[package]] name = "traitlets" -version = "5.0.5" +version = "5.1.0" description = "Traitlets Python configuration system" category = "dev" optional = false python-versions = ">=3.7" -[package.dependencies] -ipython-genutils = "*" - [package.extras] test = ["pytest"] @@ -1495,7 +1478,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.0" +version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "dev" optional = false @@ -1564,7 +1547,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "382f9225cb89e407123521c4a9ec5aedc021701f7af8ef79e2959ca5996a6be1" +content-hash = "94b3070f6b2346319330f40a7315a582f162e2ff7535b38f453d01a038fa9d03" [metadata.files] alabaster = [ @@ -1580,32 +1563,21 @@ appnope = [ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] argon2-cffi = [ - {file = "argon2-cffi-20.1.0.tar.gz", hash = "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d"}, - {file = "argon2_cffi-20.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003"}, - {file = "argon2_cffi-20.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf"}, - {file = "argon2_cffi-20.1.0-cp27-cp27m-win32.whl", hash = "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5"}, - {file = "argon2_cffi-20.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc"}, - {file = "argon2_cffi-20.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe"}, - {file = "argon2_cffi-20.1.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647"}, - {file = "argon2_cffi-20.1.0-cp35-cp35m-win32.whl", hash = "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361"}, - {file = "argon2_cffi-20.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b"}, - {file = "argon2_cffi-20.1.0-cp36-cp36m-win32.whl", hash = "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496"}, - {file = "argon2_cffi-20.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa"}, - {file = "argon2_cffi-20.1.0-cp37-abi3-macosx_10_6_intel.whl", hash = "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b"}, - {file = "argon2_cffi-20.1.0-cp37-cp37m-win32.whl", hash = "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5"}, - {file = "argon2_cffi-20.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203"}, - {file = "argon2_cffi-20.1.0-cp38-cp38-win32.whl", hash = "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78"}, - {file = "argon2_cffi-20.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2"}, - {file = "argon2_cffi-20.1.0-cp39-cp39-win32.whl", hash = "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be"}, - {file = "argon2_cffi-20.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32"}, - {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b94042e5dcaa5d08cf104a54bfae614be502c6f44c9c89ad1535b2ebdaacbd4c"}, - {file = "argon2_cffi-20.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:8282b84ceb46b5b75c3a882b28856b8cd7e647ac71995e71b6705ec06fc232c3"}, - {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3aa804c0e52f208973845e8b10c70d8957c9e5a666f702793256242e9167c4e0"}, - {file = "argon2_cffi-20.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:36320372133a003374ef4275fbfce78b7ab581440dfca9f9471be3dd9a522428"}, + {file = "argon2-cffi-21.1.0.tar.gz", hash = "sha256:f710b61103d1a1f692ca3ecbd1373e28aa5e545ac625ba067ff2feca1b2bb870"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-macosx_10_14_x86_64.whl", hash = "sha256:217b4f0f853ccbbb5045242946ad2e162e396064575860141b71a85eb47e475a"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa7e7d1fc22514a32b1761fdfa1882b6baa5c36bb3ef557bdd69e6fc9ba14a41"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-win32.whl", hash = "sha256:e4d8f0ae1524b7b0372a3e574a2561cbdddb3fdb6c28b70a72868189bda19659"}, + {file = "argon2_cffi-21.1.0-cp35-abi3-win_amd64.whl", hash = "sha256:65213a9174320a1aee03fe826596e0620783966b49eb636955958b3074e87ff9"}, + {file = "argon2_cffi-21.1.0-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:245f64a203012b144b7b8c8ea6d468cb02b37caa5afee5ba4a10c80599334f6a"}, + {file = "argon2_cffi-21.1.0-pp36-pypy36_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4ad152c418f7eb640eac41ac815534e6aa61d1624530b8e7779114ecfbf327f8"}, + {file = "argon2_cffi-21.1.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:bc513db2283c385ea4da31a2cd039c33380701f376f4edd12fe56db118a3b21a"}, + {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c7a7c8cc98ac418002090e4add5bebfff1b915ea1cb459c578cd8206fef10378"}, + {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:165cadae5ac1e26644f5ade3bd9c18d89963be51d9ea8817bd671006d7909057"}, + {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:566ffb581bbd9db5562327aee71b2eda24a1c15b23a356740abe3c011bbe0dcb"}, ] astroid = [ - {file = "astroid-2.6.6-py3-none-any.whl", hash = "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef"}, - {file = "astroid-2.6.6.tar.gz", hash = "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334"}, + {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, + {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1628,8 +1600,8 @@ black = [ {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, ] bleach = [ - {file = "bleach-4.0.0-py2.py3-none-any.whl", hash = "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d"}, - {file = "bleach-4.0.0.tar.gz", hash = "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8"}, + {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, + {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, ] certifi = [ {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, @@ -1765,8 +1737,8 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.3.3-py3-none-any.whl", hash = "sha256:4aaffc7051dd6b0e4e39170d2a69f412a21bbbf8497c85654a8d0c1fd44be534"}, - {file = "diff_cover-6.3.3.tar.gz", hash = "sha256:487b9babf6d1a7d73b9f72c2ee4cbed2840bf2f0e203e184b9ef632532115665"}, + {file = "diff_cover-6.2.1-py3-none-any.whl", hash = "sha256:cfe6333c9b83a28f91214e06e3a86108aeeb6a9a31233944ce8ef236121ba899"}, + {file = "diff_cover-6.2.1.tar.gz", hash = "sha256:b22fe97d118fadb2528bbc0a1f9fc370491b29b1b3fe2e2f1cdb94bf028814c7"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1793,8 +1765,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"}, - {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"}, + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, ] inflect = [ {file = "inflect-5.3.0-py3-none-any.whl", hash = "sha256:42560be16af702a21d43d59427f276b5aed79efb1ded9b713468c081f4353d10"}, @@ -1809,16 +1781,16 @@ ipykernel = [ {file = "ipykernel-5.5.5.tar.gz", hash = "sha256:e976751336b51082a89fc2099fb7f96ef20f535837c398df6eab1283c2070884"}, ] ipython = [ - {file = "ipython-7.26.0-py3-none-any.whl", hash = "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362"}, - {file = "ipython-7.26.0.tar.gz", hash = "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e"}, + {file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"}, + {file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] ipywidgets = [ - {file = "ipywidgets-7.6.3-py2.py3-none-any.whl", hash = "sha256:e6513cfdaf5878de30f32d57f6dc2474da395a2a2991b94d487406c0ab7f55ca"}, - {file = "ipywidgets-7.6.3.tar.gz", hash = "sha256:9f1a43e620530f9e570e4a493677d25f08310118d315b00e25a18f12913c41f0"}, + {file = "ipywidgets-7.6.4-py2.py3-none-any.whl", hash = "sha256:3ffd1baa741eb631e7a3a69d4df290de074ef697e0ef3176e33361b44cd91711"}, + {file = "ipywidgets-7.6.4.tar.gz", hash = "sha256:028bf014a0b1d77cb676fe163115f145aacdde0bb9a51c4166940e5b62a7d1d0"}, ] isort = [ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, @@ -1846,8 +1818,8 @@ jupyter = [ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, ] jupyter-client = [ - {file = "jupyter_client-7.0.1-py3-none-any.whl", hash = "sha256:07b9566979546004c089afe7c9bf9e96224ec5f8421fe0ae460759fa593c6b1d"}, - {file = "jupyter_client-7.0.1.tar.gz", hash = "sha256:48822a93d9d75daa5fde235c35cf7a92fc979384735962501d4eb60b197fb43a"}, + {file = "jupyter_client-7.0.2-py3-none-any.whl", hash = "sha256:37a30c13d3655b819add61c830594090af7fca40cd2d74f41cad9e2e12118501"}, + {file = "jupyter_client-7.0.2.tar.gz", hash = "sha256:0c6cabd07e003a2e9692394bf1ae794188ad17d2e250ed747232d7a473aa772c"}, ] jupyter-console = [ {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, @@ -1862,42 +1834,54 @@ jupyterlab-pygments = [ {file = "jupyterlab_pygments-0.1.2.tar.gz", hash = "sha256:cfcda0873626150932f438eccf0f8bf22bfa92345b814890ab360d666b254146"}, ] jupyterlab-widgets = [ - {file = "jupyterlab_widgets-1.0.0-py3-none-any.whl", hash = "sha256:caeaf3e6103180e654e7d8d2b81b7d645e59e432487c1d35a41d6d3ee56b3fef"}, - {file = "jupyterlab_widgets-1.0.0.tar.gz", hash = "sha256:5c1a29a84d3069208cb506b10609175b249b6486d6b1cbae8fcde2a11584fb78"}, + {file = "jupyterlab_widgets-1.0.1-py3-none-any.whl", hash = "sha256:841925a349bd9a9197c5506bd5461a321b09e6659a9b179a0096b561a92898c3"}, + {file = "jupyterlab_widgets-1.0.1.tar.gz", hash = "sha256:f94fb7fa1ddc8668e3f98d67a97cabe322e8d04b78b9eb988c7fde415d7a02df"}, ] kiwisolver = [ - {file = "kiwisolver-1.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"}, - {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0"}, - {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21"}, - {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05"}, - {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b"}, - {file = "kiwisolver-1.3.1-cp36-cp36m-win32.whl", hash = "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9"}, - {file = "kiwisolver-1.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc"}, - {file = "kiwisolver-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454"}, - {file = "kiwisolver-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72"}, - {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3"}, - {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131"}, - {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de"}, - {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18"}, - {file = "kiwisolver-1.3.1-cp38-cp38-win32.whl", hash = "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81"}, - {file = "kiwisolver-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e"}, - {file = "kiwisolver-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000"}, - {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598"}, - {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882"}, - {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621"}, - {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54"}, - {file = "kiwisolver-1.3.1-cp39-cp39-win32.whl", hash = "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030"}, - {file = "kiwisolver-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6"}, - {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d"}, - {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3"}, - {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6"}, - {file = "kiwisolver-1.3.1.tar.gz", hash = "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, + {file = "kiwisolver-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402"}, + {file = "kiwisolver-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0"}, + {file = "kiwisolver-1.3.2-cp310-cp310-win32.whl", hash = "sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977"}, + {file = "kiwisolver-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-win32.whl", hash = "sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589"}, + {file = "kiwisolver-1.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84"}, + {file = "kiwisolver-1.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b"}, + {file = "kiwisolver-1.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28"}, + {file = "kiwisolver-1.3.2-cp38-cp38-win32.whl", hash = "sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c"}, + {file = "kiwisolver-1.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f"}, + {file = "kiwisolver-1.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b"}, + {file = "kiwisolver-1.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439"}, + {file = "kiwisolver-1.3.2-cp39-cp39-win32.whl", hash = "sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9"}, + {file = "kiwisolver-1.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223"}, + {file = "kiwisolver-1.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560"}, + {file = "kiwisolver-1.3.2.tar.gz", hash = "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, @@ -2052,8 +2036,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] myst-parser = [ - {file = "myst-parser-0.15.1.tar.gz", hash = "sha256:7c3c78a36c4bc30ce6a67933ebd800a880c8d81f1688fad5c2ebd82cddbc1603"}, - {file = "myst_parser-0.15.1-py3-none-any.whl", hash = "sha256:e8baa9959dac0bcf0f3ea5fc32a1a28792959471d8a8094e3ed5ee0de9733ade"}, + {file = "myst-parser-0.15.2.tar.gz", hash = "sha256:f7f3b2d62db7655cde658eb5d62b2ec2a4631308137bd8d10f296a40d57bbbeb"}, + {file = "myst_parser-0.15.2-py3-none-any.whl", hash = "sha256:40124b6f27a4c42ac7f06b385e23a9dcd03d84801e9c7130b59b3729a554b1f9"}, ] nbclient = [ {file = "nbclient-0.5.4-py3-none-any.whl", hash = "sha256:95a300c6fbe73721736cf13972a46d8d666f78794b832866ed7197a504269e11"}, @@ -2182,9 +2166,13 @@ pillow = [ {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, ] +platformdirs = [ + {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, + {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, +] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] prometheus-client = [ {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"}, @@ -2202,9 +2190,6 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] -py-cpuinfo = [ - {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, -] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -2253,8 +2238,8 @@ pygraphviz = [ {file = "pygraphviz-1.7.zip", hash = "sha256:a7bec6609f37cf1e64898c59f075afd659106cf9356c5f387cecaa2e0cdb2304"}, ] pylint = [ - {file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"}, - {file = "pylint-2.9.6.tar.gz", hash = "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"}, + {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, + {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -2284,12 +2269,8 @@ pyrsistent = [ {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, -] -pytest-benchmark = [ - {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, - {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, @@ -2316,11 +2297,11 @@ pywin32 = [ {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"}, ] pywinpty = [ - {file = "pywinpty-1.1.3-cp36-none-win_amd64.whl", hash = "sha256:81dc6f16d917b756e06fc58943e9750d59dbefc0ffd2086871d3fa5f33824446"}, - {file = "pywinpty-1.1.3-cp37-none-win_amd64.whl", hash = "sha256:54557887e712ea3215ab0d9f089ed55a6cc8d826cd5d1e340d75300654c9663f"}, - {file = "pywinpty-1.1.3-cp38-none-win_amd64.whl", hash = "sha256:f5e25197397f1fef0362caf3eb89f25441827a1e48bf15827c27021592fd2160"}, - {file = "pywinpty-1.1.3-cp39-none-win_amd64.whl", hash = "sha256:b767276224f86b7560eb9173ba7956758cafcdfab97bb33837d42d2a0f1dbf67"}, - {file = "pywinpty-1.1.3.tar.gz", hash = "sha256:3a1d57b338390333812a5eed31c93c7d8ba82b131078063703e731946d90c9f2"}, + {file = "pywinpty-1.1.4-cp36-none-win_amd64.whl", hash = "sha256:fb975976ad92be44801de95fdf2b0366747767cb0528478553aff85dd63ebb09"}, + {file = "pywinpty-1.1.4-cp37-none-win_amd64.whl", hash = "sha256:5d25b30a2f87105778bc2f57cb1271f58aaa25568921ef042faf001b3b0a7307"}, + {file = "pywinpty-1.1.4-cp38-none-win_amd64.whl", hash = "sha256:c5c3550100689632f6663f39865ef8716835dab1838a9eb9b472644af92673f8"}, + {file = "pywinpty-1.1.4-cp39-none-win_amd64.whl", hash = "sha256:ad60a336d92ac38e2159320db6d5999c4c2726a141c3ed3f9694021feb6a234e"}, + {file = "pywinpty-1.1.4.tar.gz", hash = "sha256:cc700c9d5a9fcebf677ac93a4943ca9a24db6e2f11a5f0e7e8e226184c5036f7"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -2401,39 +2382,47 @@ qtpy = [ {file = "QtPy-1.10.0.tar.gz", hash = "sha256:3d20f010caa3b2c04835d6a2f66f8873b041bdaf7a76085c2a0d7890cdd65ea9"}, ] regex = [ - {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, - {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, - {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, - {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, - {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, - {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, - {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, - {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, - {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, - {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, - {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, - {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, - {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, - {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, - {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, - {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, - {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, - {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, - {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, - {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, - {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, - {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, - {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, - {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, - {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, - {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, - {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, - {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, - {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, - {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, - {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, - {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, - {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, + {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, + {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, + {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, + {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, + {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, + {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, + {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, + {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, + {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, + {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, + {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, + {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, + {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, + {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, + {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, + {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, @@ -2543,8 +2532,8 @@ tornado = [ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] traitlets = [ - {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, - {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, + {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, + {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, ] typed-ast = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, @@ -2579,9 +2568,9 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, diff --git a/pyproject.toml b/pyproject.toml index a26855618..4fa5d1cad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ black = "21.7b0" pylint = "^2.9.3" pytest = "^6.2.4" pytest-cov = "^2.12.1" -pytest-benchmark = "^3.4.1" diff-cover = "^6.2.0" mypy = "^0.910" pydocstyle = "^6.1.1" From b3f266c1c0ab467b1b7f3a9cf51791cb7bef6373 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 3 Sep 2021 10:59:59 +0300 Subject: [PATCH 0197/1104] fix: fix new pylint warnings after version update --- concrete/common/compilation/artifacts.py | 14 +++++++------- concrete/common/optimization/topological.py | 2 +- concrete/common/tracing/tracing_helpers.py | 2 +- concrete/numpy/compile.py | 8 ++++++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index a0f5760f0..152b48b62 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -130,11 +130,11 @@ class CompilationArtifacts: shutil.rmtree(output_directory) output_directory.mkdir() - with open(output_directory.joinpath("environment.txt"), "w") as f: + with open(output_directory.joinpath("environment.txt"), "w", encoding="utf-8") as f: f.write(f"{platform.platform()} {platform.version()}\n") f.write(f"Python {platform.python_version()}\n") - with open(output_directory.joinpath("requirements.txt"), "w") as f: + with open(output_directory.joinpath("requirements.txt"), "w", encoding="utf-8") as f: # example `pip list` output # Package Version @@ -166,11 +166,11 @@ class CompilationArtifacts: f.write(f"{name}=={version}\n") if self.source_code_of_the_function_to_compile is not None: - with open(output_directory.joinpath("function.txt"), "w") as f: + with open(output_directory.joinpath("function.txt"), "w", encoding="utf-8") as f: f.write(self.source_code_of_the_function_to_compile) if len(self.parameters_of_the_function_to_compile) > 0: - with open(output_directory.joinpath("parameters.txt"), "w") as f: + with open(output_directory.joinpath("parameters.txt"), "w", encoding="utf-8") as f: for name, parameter in self.parameters_of_the_function_to_compile.items(): f.write(f"{name} :: {parameter}\n") @@ -182,12 +182,12 @@ class CompilationArtifacts: textual_representations = self.textual_representations_of_operation_graphs.items() for index, (name, representation) in enumerate(textual_representations): identifier = CompilationArtifacts._identifier(index, name) - with open(output_directory.joinpath(f"{identifier}.txt"), "w") as f: + with open(output_directory.joinpath(f"{identifier}.txt"), "w", encoding="utf-8") as f: f.write(f"{representation}\n") if self.bounds_of_the_final_operation_graph is not None: assert self.final_operation_graph is not None - with open(output_directory.joinpath("bounds.txt"), "w") as f: + with open(output_directory.joinpath("bounds.txt"), "w", encoding="utf-8") as f: # TODO: # if nx.topological_sort is not deterministic between calls, # the lines below will not work properly @@ -199,7 +199,7 @@ class CompilationArtifacts: if self.mlir_of_the_final_operation_graph is not None: assert self.final_operation_graph is not None - with open(output_directory.joinpath("mlir.txt"), "w") as f: + with open(output_directory.joinpath("mlir.txt"), "w", encoding="utf-8") as f: f.write(self.mlir_of_the_final_operation_graph) @staticmethod diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index f80ec2161..0638b1e16 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -222,7 +222,7 @@ def find_float_subgraph_with_unique_terminal_node( float_subgraph_start_nodes: Set[ir.IntermediateNode] = set() subgraph_all_nodes: Set[ir.IntermediateNode] = set() while current_nodes: - next_nodes: Dict[ir.IntermediateNode, None] = dict() + next_nodes: Dict[ir.IntermediateNode, None] = {} for node in current_nodes: subgraph_all_nodes.add(node) predecessors = nx_graph.pred[node] diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index 8e0824c47..bf8748fe2 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -105,7 +105,7 @@ def create_graph_from_output_tracers( while current_tracers: # use dict as ordered set - next_tracers: Dict[BaseTracer, None] = dict() + next_tracers: Dict[BaseTracer, None] = {} for tracer in current_tracers: current_ir_node = tracer.traced_computation graph.add_node(current_ir_node) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index e3de2a337..8ac26c9e5 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -185,7 +185,9 @@ def compile_numpy_function_into_op_graph( if compilation_configuration.dump_artifacts_on_unexpected_failures: compilation_artifacts.export() - with open(compilation_artifacts.output_directory.joinpath("traceback.txt"), "w") as f: + + traceback_path = compilation_artifacts.output_directory.joinpath("traceback.txt") + with open(traceback_path, "w", encoding="utf-8") as f: f.write(traceback.format_exc()) raise @@ -304,7 +306,9 @@ def compile_numpy_function( if compilation_configuration.dump_artifacts_on_unexpected_failures: compilation_artifacts.export() - with open(compilation_artifacts.output_directory.joinpath("traceback.txt"), "w") as f: + + traceback_path = compilation_artifacts.output_directory.joinpath("traceback.txt") + with open(traceback_path, "w", encoding="utf-8") as f: f.write(traceback.format_exc()) raise From d02e848fca34a81e5292787ec58da4a904e350f3 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 7 Sep 2021 17:26:17 +0300 Subject: [PATCH 0198/1104] refactor: update benchmark infrastructure to be compatible with the new progress tracker --- .gitignore | 4 +- Makefile | 8 +- benchmarks/linear_regression.py | 163 ++++++++++++ benchmarks/logistic_regression.py | 234 +++++++++++++++++ benchmarks/single_table_lookup.py | 48 ++++ benchmarks/x_plus_42.py | 43 +++ benchmarks/x_plus_y.py | 45 ++++ benchmarks/x_to_the_power_of_2.py | 43 +++ poetry.lock | 122 +++++---- pylintrc | 2 +- pyproject.toml | 1 + script/progress_tracker_utils/measure.py | 316 +++++++++++++++++++++++ 12 files changed, 979 insertions(+), 50 deletions(-) create mode 100644 benchmarks/linear_regression.py create mode 100644 benchmarks/logistic_regression.py create mode 100644 benchmarks/single_table_lookup.py create mode 100644 benchmarks/x_plus_42.py create mode 100644 benchmarks/x_plus_y.py create mode 100644 benchmarks/x_to_the_power_of_2.py create mode 100644 script/progress_tracker_utils/measure.py diff --git a/.gitignore b/.gitignore index 5609029c7..682d620d1 100644 --- a/.gitignore +++ b/.gitignore @@ -132,8 +132,8 @@ dmypy.json # Pyre type checker .pyre/ -# pytest-benchmark results -.benchmarks +# Benchmark results +.benchmarks.json # concrete compilation artifacts .artifacts diff --git a/Makefile b/Makefile index 2f59e6fd6..f9889d491 100644 --- a/Makefile +++ b/Makefile @@ -44,8 +44,9 @@ pylint_tests: .PHONY: pylint_tests pylint_benchmarks: - @# Disable duplicate code detection in benchmarks - poetry run pylint --disable=R0801 --rcfile=pylintrc benchmarks + @# Disable duplicate code detection, docstring requirement, too many locals/statements + poetry run pylint --disable=R0801,R0914,R0915,C0103,C0114,C0115,C0116 \ + --rcfile=pylintrc benchmarks .PHONY: pylint_benchmarks flake8: @@ -168,7 +169,8 @@ pytest_nb: .PHONY: pytest_nb benchmark: - poetry run pytest benchmarks/ --benchmark-save=findings + poetry run python script/progress_tracker_utils/measure.py benchmarks \ + --output .benchmarks.json .PHONY: benchmark jupyter: diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py new file mode 100644 index 000000000..34b51b38a --- /dev/null +++ b/benchmarks/linear_regression.py @@ -0,0 +1,163 @@ +# Target: Linear Regression + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32) + y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32) + + class Model: + w = None + b = None + + def fit(self, x, y): + a = np.ones((x.shape[0], x.shape[1] + 1), dtype=np.float32) + a[:, 1:] = x + + regularization_contribution = np.identity(x.shape[1] + 1, dtype=np.float32) + regularization_contribution[0][0] = 0 + + parameters = np.linalg.pinv(a.T @ a + regularization_contribution) @ a.T @ y + + self.b = parameters[0] + self.w = parameters[1:].reshape(-1, 1) + + return self + + def evaluate(self, x): + return x @ self.w + self.b + + model = Model().fit(x, y) + + class QuantizationParameters: + def __init__(self, q, zp, n): + self.q = q + self.zp = zp + self.n = n + + class QuantizedArray: + def __init__(self, values, parameters): + self.values = np.array(values) + self.parameters = parameters + + @staticmethod + def of(x, n): + if not isinstance(x, np.ndarray): + x = np.array(x) + + min_x = x.min() + max_x = x.max() + + if min_x == max_x: + + if min_x == 0.0: + q_x = 1 + zp_x = 0 + x_q = np.zeros(x.shape, dtype=np.uint) + + elif min_x < 0.0: + q_x = abs(1 / min_x) + zp_x = -1 + x_q = np.zeros(x.shape, dtype=np.uint) + + else: + q_x = 1 / min_x + zp_x = 0 + x_q = np.ones(x.shape, dtype=np.uint) + + else: + q_x = (2 ** n - 1) / (max_x - min_x) + zp_x = int(round(min_x * q_x)) + x_q = ((q_x * x) - zp_x).round().astype(np.uint) + + return QuantizedArray(x_q, QuantizationParameters(q_x, zp_x, n)) + + def dequantize(self): + return (self.values.astype(np.float32) + float(self.parameters.zp)) / self.parameters.q + + class QuantizedFunction: + def __init__(self, table): + self.table = table + + @staticmethod + def of(f, input_bits, output_bits): + domain = np.array(range(2 ** input_bits), dtype=np.uint) + table = f(domain).round().clip(0, 2 ** output_bits - 1).astype(np.uint) + return QuantizedFunction(table) + + parameter_bits = 1 + + w_q = QuantizedArray.of(model.w, parameter_bits) + b_q = QuantizedArray.of(model.b, parameter_bits) + + input_bits = 6 + + x_q = QuantizedArray.of(x, input_bits) + + output_bits = 7 + + min_y = y.min() + max_y = y.max() + + n_y = output_bits + q_y = (2 ** n_y - 1) / (max_y - min_y) + zp_y = int(round(min_y * q_y)) + y_parameters = QuantizationParameters(q_y, zp_y, n_y) + + q_x = x_q.parameters.q + q_w = w_q.parameters.q + q_b = b_q.parameters.q + + zp_x = x_q.parameters.zp + zp_w = w_q.parameters.zp + zp_b = b_q.parameters.zp + + x_q = x_q.values + w_q = w_q.values + b_q = b_q.values + + c1 = q_y / (q_x * q_w) + c2 = w_q + zp_w + c3 = (q_x * q_w / q_b) * (b_q + zp_b) + c4 = min_y * q_y + + f_q = QuantizedFunction.of( + lambda intermediate: (c1 * (intermediate + c3)) - c4, + input_bits + parameter_bits, + output_bits, + ) + + table = hnp.LookupTable([int(entry) for entry in f_q.table]) + + w_0 = int(c2.flatten()[0]) + + def function_to_compile(x_0): + return table[(x_0 + zp_x) * w_0] + + dataset = [] + for x_i in x_q: + dataset.append((int(x_i[0]),)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits))}, + iter(dataset), + ) + # Measure: End + + loss = 0 + for x_i, y_i in zip(x_q, y): + # Measure: Evaluation Time (ms) + prediction = QuantizedArray(engine.run(*x_i), y_parameters).dequantize() + # Measure: End + loss += (prediction - y_i) ** 2 + + # Measure: Loss = loss / len(y) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py new file mode 100644 index 000000000..c6f1be799 --- /dev/null +++ b/benchmarks/logistic_regression.py @@ -0,0 +1,234 @@ +# Target: Logistic Regression + +import numpy as np +import torch + +import concrete.numpy as hnp + + +def main(): + x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float() + y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float() + + class Model(torch.nn.Module): + def __init__(self, n): + super().__init__() + self.fc = torch.nn.Linear(n, 1) + + def forward(self, x): + output = torch.sigmoid(self.fc(x)) + return output + + model = Model(x.shape[1]) + + optimizer = torch.optim.SGD(model.parameters(), lr=1) + criterion = torch.nn.BCELoss() + + epochs = 1501 + for e in range(1, epochs + 1): + optimizer.zero_grad() + + out = model(x) + loss = criterion(out, y) + + loss.backward() + optimizer.step() + + if e % 100 == 1 or e == epochs: + print("Epoch:", e, "|", "Loss:", loss.item()) + + w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1)) + b = model.fc.bias.flatten().tolist()[0] + + x = x.detach().numpy() + y = y.detach().numpy().flatten() + + class QuantizationParameters: + def __init__(self, q, zp, n): + self.q = q + self.zp = zp + self.n = n + + class QuantizedArray: + def __init__(self, values, parameters): + self.values = np.array(values) + self.parameters = parameters + + @staticmethod + def of(x, n): + if not isinstance(x, np.ndarray): + x = np.array(x) + + min_x = x.min() + max_x = x.max() + + if min_x == max_x: + + if min_x == 0.0: + q_x = 1 + zp_x = 0 + x_q = np.zeros(x.shape, dtype=np.uint) + + elif min_x < 0.0: + q_x = abs(1 / min_x) + zp_x = -1 + x_q = np.zeros(x.shape, dtype=np.uint) + + else: + q_x = 1 / min_x + zp_x = 0 + x_q = np.ones(x.shape, dtype=np.uint) + + else: + q_x = (2 ** n - 1) / (max_x - min_x) + zp_x = int(round(min_x * q_x)) + x_q = ((q_x * x) - zp_x).round().astype(np.uint) + + return QuantizedArray(x_q, QuantizationParameters(q_x, zp_x, n)) + + def dequantize(self): + return (self.values.astype(np.float32) + float(self.parameters.zp)) / self.parameters.q + + def affine(self, w, b, min_y, max_y, n_y): + x_q = self.values + w_q = w.values + b_q = b.values + + q_x = self.parameters.q + q_w = w.parameters.q + q_b = b.parameters.q + + zp_x = self.parameters.zp + zp_w = w.parameters.zp + zp_b = b.parameters.zp + + q_y = (2 ** n_y - 1) / (max_y - min_y) + zp_y = int(round(min_y * q_y)) + + y_q = (q_y / (q_x * q_w)) * ( + (x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b) + ) + y_q -= min_y * q_y + y_q = y_q.round().clip(0, 2 ** n_y - 1).astype(np.uint) + + return QuantizedArray(y_q, QuantizationParameters(q_y, zp_y, n_y)) + + class QuantizedFunction: + def __init__(self, table, input_parameters=None, output_parameters=None): + self.table = table + self.input_parameters = input_parameters + self.output_parameters = output_parameters + + @staticmethod + def of(f, input_bits, output_bits): + domain = np.array(range(2 ** input_bits), dtype=np.uint) + table = f(domain).round().clip(0, 2 ** output_bits - 1).astype(np.uint) + return QuantizedFunction(table) + + @staticmethod + def plain(f, input_parameters, output_bits): + n = input_parameters.n + + domain = np.array(range(2 ** n), dtype=np.uint) + inputs = QuantizedArray(domain, input_parameters).dequantize() + + outputs = f(inputs) + quantized_outputs = QuantizedArray.of(outputs, output_bits) + + table = quantized_outputs.values + output_parameters = quantized_outputs.parameters + + return QuantizedFunction(table, input_parameters, output_parameters) + + def apply(self, x): + assert x.parameters == self.input_parameters + return QuantizedArray(self.table[x.values], self.output_parameters) + + parameter_bits = 1 + + w_q = QuantizedArray.of(w, parameter_bits) + b_q = QuantizedArray.of(b, parameter_bits) + + input_bits = 5 + + x_q = QuantizedArray.of(x, input_bits) + + output_bits = 7 + + intermediate = x @ w + b + intermediate_q = x_q.affine(w_q, b_q, intermediate.min(), intermediate.max(), output_bits) + + n_y = output_bits + q_y = (2 ** output_bits - 1) / (intermediate.max() - intermediate.min()) + zp_y = int(round(intermediate.min() * q_y)) + y_parameters = QuantizationParameters(q_y, zp_y, n_y) + + q_x = x_q.parameters.q + q_w = w_q.parameters.q + q_b = b_q.parameters.q + + zp_x = x_q.parameters.zp + zp_w = w_q.parameters.zp + zp_b = b_q.parameters.zp + + x_q = x_q.values + w_q = w_q.values + b_q = b_q.values + + c1 = q_y / (q_x * q_w) + c2 = w_q + zp_w + c3 = (q_x * q_w / q_b) * (b_q + zp_b) + c4 = intermediate.min() * q_y + + def f(x): + values = ((c1 * (x + c3)) - c4).round().clip(0, 2 ** output_bits - 1).astype(np.uint) + after_affine_q = QuantizedArray(values, intermediate_q.parameters) + + sigmoid = QuantizedFunction.plain( + lambda x: 1 / (1 + np.exp(-x)), + after_affine_q.parameters, + output_bits, + ) + y_q = sigmoid.apply(after_affine_q) + + return y_q.values + + f_q = QuantizedFunction.of(f, output_bits, output_bits) + + table = hnp.LookupTable([int(entry) for entry in f_q.table]) + + w_0 = int(c2.flatten()[0]) + w_1 = int(c2.flatten()[1]) + + def function_to_compile(x_0, x_1): + return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)] + + dataset = [] + for x_i in x_q: + dataset.append((int(x_i[0]), int(x_i[1]))) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + { + "x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), + "x_1": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), + }, + iter(dataset), + ) + # Measure: End + + correct = 0 + for x_i, y_i in zip(x_q, y): + # Measure: Evaluation Time (ms) + prediction = round(QuantizedArray(engine.run(*x_i), y_parameters).dequantize()) + # Measure: End + + if prediction == y_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(y)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/single_table_lookup.py b/benchmarks/single_table_lookup.py new file mode 100644 index 000000000..3a8fc23cf --- /dev/null +++ b/benchmarks/single_table_lookup.py @@ -0,0 +1,48 @@ +# Target: Single Table Lookup + +import random + +import concrete.numpy as hnp + + +def main(): + input_bits = 3 + + entries = [i ** 2 for i in range(2 ** input_bits)] + table = hnp.LookupTable(entries) + + def function_to_compile(x): + return table[x] + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + iter([(i,) for i in range(2 ** input_bits)]), + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(10): + sample_x = random.randint(0, (2 ** input_bits) - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py new file mode 100644 index 000000000..aeb840a74 --- /dev/null +++ b/benchmarks/x_plus_42.py @@ -0,0 +1,43 @@ +# Target: x + 42 + +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + iter([(6,), (1,), (5,), (2,)]), + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** 3 - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_y.py b/benchmarks/x_plus_y.py new file mode 100644 index 000000000..d668259f4 --- /dev/null +++ b/benchmarks/x_plus_y.py @@ -0,0 +1,45 @@ +# Target: x + y + +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x + y + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + iter([(6, 1), (1, 4), (5, 3), (2, 0), (7, 7)]), + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** 3 - 1) + sample_y = random.randint(0, 2 ** 3 - 1) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_to_the_power_of_2.py b/benchmarks/x_to_the_power_of_2.py new file mode 100644 index 000000000..33c8e84b4 --- /dev/null +++ b/benchmarks/x_to_the_power_of_2.py @@ -0,0 +1,43 @@ +# Target: x**2 + +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x ** 2 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + iter([(6,), (1,), (5,), (2,)]), + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** 3 - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index f9d336b67..2bc3c6302 100644 --- a/poetry.lock +++ b/poetry.lock @@ -939,7 +939,7 @@ python-versions = "*" [[package]] name = "pillow" -version = "8.3.1" +version = "8.3.2" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1228,7 +1228,7 @@ test = ["flaky", "pytest", "pytest-qt"] [[package]] name = "qtpy" -version = "1.10.0" +version = "1.11.0" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." category = "dev" optional = false @@ -1457,6 +1457,22 @@ category = "dev" optional = false python-versions = ">= 3.5" +[[package]] +name = "tqdm" +version = "4.62.2" +description = "Fast, Extensible Progress Meter" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +telegram = ["requests"] + [[package]] name = "traitlets" version = "5.1.0" @@ -1547,7 +1563,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "94b3070f6b2346319330f40a7315a582f162e2ff7535b38f453d01a038fa9d03" +content-hash = "bc8dd9b8a8b3320ee517534a31a18bc11675799ccf6f0d2972c1d283ac50b720" [metadata.files] alabaster = [ @@ -2126,45 +2142,59 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pillow = [ - {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, - {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, - {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, - {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, - {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, - {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, - {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, - {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, - {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"}, - {file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"}, - {file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"}, - {file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"}, - {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"}, - {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"}, - {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"}, - {file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"}, - {file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"}, - {file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"}, - {file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"}, - {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"}, - {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"}, - {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"}, - {file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"}, - {file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"}, - {file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"}, - {file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"}, - {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"}, - {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"}, - {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"}, - {file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"}, - {file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"}, - {file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"}, - {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"}, - {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, - {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] platformdirs = [ {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, @@ -2378,8 +2408,8 @@ qtconsole = [ {file = "qtconsole-5.1.1.tar.gz", hash = "sha256:bbc34bca14f65535afcb401bc74b752bac955e5313001ba640383f7e5857dc49"}, ] qtpy = [ - {file = "QtPy-1.10.0-py2.py3-none-any.whl", hash = "sha256:f683ce6cd825ba8248a798bf1dfa1a07aca387c88ae44fa5479537490aace7be"}, - {file = "QtPy-1.10.0.tar.gz", hash = "sha256:3d20f010caa3b2c04835d6a2f66f8873b041bdaf7a76085c2a0d7890cdd65ea9"}, + {file = "QtPy-1.11.0-py2.py3-none-any.whl", hash = "sha256:bd8baebb80c4d0d97e4e5a5cf15695522f6acc1fecc20b94a70a01ddf6c9e27e"}, + {file = "QtPy-1.11.0.tar.gz", hash = "sha256:bbd61f8d6480a01cec39ad94249dbde7d0a8fce2aca61ff5037b645c4fd13e02"}, ] regex = [ {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, @@ -2531,6 +2561,10 @@ tornado = [ {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] +tqdm = [ + {file = "tqdm-4.62.2-py2.py3-none-any.whl", hash = "sha256:80aead664e6c1672c4ae20dc50e1cdc5e20eeff9b14aa23ecd426375b28be588"}, + {file = "tqdm-4.62.2.tar.gz", hash = "sha256:a4d6d112e507ef98513ac119ead1159d286deab17dffedd96921412c2d236ff5"}, +] traitlets = [ {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, diff --git a/pylintrc b/pylintrc index 65bbb6a0b..c85b6eb78 100644 --- a/pylintrc +++ b/pylintrc @@ -438,7 +438,7 @@ contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. -generated-members= +generated-members=torch # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). diff --git a/pyproject.toml b/pyproject.toml index 4fa5d1cad..e544028f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ flake8-bugbear = "^21.4.3" Sphinx = "^4.1.1" sphinx-rtd-theme = "^0.5.2" myst-parser = "^0.15.1" +tqdm = "^4.62.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py new file mode 100644 index 000000000..5193f9a08 --- /dev/null +++ b/script/progress_tracker_utils/measure.py @@ -0,0 +1,316 @@ +import argparse +import json +import os +import pathlib +import subprocess +import urllib +import tqdm + + +def name_to_id(name): + """Convert a human readable name to a url friendly id (e.g., `x + y` to `x-plus-y`)""" + + name = name.replace("**", "-to-the-power-of-") + name = name.replace("+", "plus") + name = name.replace("-", "minus") + name = name.replace("*", "times") + name = name.replace("/", "over") + name = name.replace("%", "percent") + name = name.replace(" ", "-") + name = name.replace("(", "") + name = name.replace(")", "") + + return urllib.parse.quote_plus(name.lower()) + + +def identify_metrics(script, lines, metrics): + """Identify the metrics of a script and make sure the annotations are well-formed""" + + # Create a flag to detect `# Measure: End` without a measurement start + in_measurement = False + + # Create a variable to remember the indentation of the start of the last measurement + measurement_indentation = 0 + + # Create a variable to remember the line number of the start of the last measurement + measurement_line = 0 + + # Identify measurements and store their name and id in `metrics` + for index, line in enumerate(lines): + # Get the indentation of the line + indentation = len(line) - len(line.lstrip()) + + # Strip the line for easier processing + line = line.strip() + + # Check whether the line is a special line or not + if line == "# Measure: End": + # Make sure a measurement is active already + if not in_measurement: + raise SyntaxError( + f"Measurements cannot end before they are defined " + f"(at line {index + 1} of {script})", + ) + + # Make sure indentation of the current line + # matches the indentation of the active measurement line + if indentation != measurement_indentation: + raise SyntaxError( + f"Measurements should finish with the same indentation as they are defined " + f"(at lines {measurement_line} and {index + 1} of {script})", + ) + + # Set in_measurement to false as the active measurement has ended + in_measurement = False + elif line.startswith("# Measure:"): + # Make sure a measurement is not active already + if in_measurement: + raise SyntaxError( + f"Nested measurements are not supported " + f"(at lines {measurement_line} and {index + 1} of {script})", + ) + + # Extract the measurement details + measurement_details = line.replace("# Measure:", "").split("=") + + # Extract metric name and id + metric_label = measurement_details[0].strip() + metric_id = name_to_id(metric_label) + + # Add metric id and metric name to `metrics` + metrics[metric_id] = metric_label + + # Check if the measurement is a timing measurement (does not contain `= expression`) + if len(measurement_details) == 1: + # We need to see an end in the upcoming lines so update variables accordingly + in_measurement = True + measurement_line = index + 1 + measurement_indentation = indentation + + # Make sure there isn't an active measurement that hasn't finished + if in_measurement: + raise SyntaxError( + f"Unfinished measurements are not supported " + f"(at line {measurement_line} of {script})", + ) + + +def create_modified_script(script_without_extension, lines, metrics): + """Create a modified version of the script which can be used to perform measurements""" + + with open(f"{script_without_extension}.measure.py", "w") as f: + # Import must-have libraries + f.write("import json\n") + f.write("import time\n") + f.write("\n") + + # Create a measurement dictionary to accumulate values + f.write("_measurements_ = {\n") + for metric_id in metrics.keys(): + f.write(f" \"{metric_id}\": [],\n") + f.write("}\n") + + # Create a variable to hold the id of the current metric + # This is required to determine where to save the measured value + current_metric_id = "" + + # Copy the lines of the original script into the new script + for line in lines[1:]: + # And modify special lines along the way + if line.strip() == "# Measure: End": + # Replace `# Measure: End` with + # + # _end_ = time.time() + # _measurements_["id"].append((_end_ - _start_) * 1000) + + index = line.find("# Measure: End") + line = line[:index] + + f.write(f"{line}_end_ = time.time()\n") + + value = "(_end_ - _start_) * 1000" + line += f"_measurements_[\"{current_metric_id}\"].append({value})\n" + elif line.strip().startswith("# Measure:"): + # Replace `# Measure: ...` with + # + # _start_ = time.time() + + # Replace `# Measure: ... = expression` with + # + # _measurements_["id"].append(expression) + + metric_details = line.replace("# Measure:", "").split("=") + metric_label = metric_details[0].strip() + metric_id = name_to_id(metric_label) + + index = line.find("# Measure:") + line = line[:index] + + if len(metric_details) == 1: + current_metric_id = metric_id + line += "_start_ = time.time()\n" + else: + value = metric_details[1] + line += f"_measurements_[\"{metric_id}\"].append({value.strip()})\n" + + # Write the possibly replaced line back + f.write(line) + + # Dump measurements to a temporary file after the script is executed from start to end + f.write("\n") + f.write(f"with open(\"{script_without_extension}.measurements\", \"w\") as f:\n") + f.write(f" json.dump(_measurements_, f, indent=2)\n") + + +def perform_measurements(script, script_without_extension, target_id, metrics, samples, result): + """Run the modified script multiple times and update the result""" + + # Create a flag to keep track of the working status + working = True + + print() + print(script) + print("-" * len(str(script))) + + # Run the modified script `samples` times and accumulate measurements + measurements = {metric_id: [] for metric_id in metrics.keys()} + with tqdm.tqdm(total=samples) as pbar: + for i in range(samples): + # Create the subprocess + process = subprocess.run( + ["python", f"{script_without_extension}.measure.py"], + capture_output=True, + ) + + # Print sample information + pbar.write(f" Sample {i + 1}") + pbar.write(f" {'-' * len(f'Sample {i + 1}')}") + + # If the script raised an exception, discard everything for now + if process.returncode != 0: + working = False + + pbar.write(f" Failed (exited with {process.returncode})") + pbar.write(f"") + + pbar.update(samples) + break + + # Read the measurements and delete the temporary file + with open(f"{script_without_extension}.measurements") as f: + results = json.load(f) + os.unlink(f"{script_without_extension}.measurements") + + # Add the `results` of the current run to `measurements` + for metric_id in metrics.keys(): + average = sum(results[metric_id]) / len(results[metric_id]) + pbar.write(f" {metrics[metric_id]} = {average}") + + for measurement in results[metric_id]: + measurements[metric_id].append(measurement) + pbar.write(f"") + + pbar.update(1) + print() + + result["targets"][target_id]["working"] = working + + if working: + # Take average of all metrics and store them in `result` + result["targets"][target_id]["measurements"].update({ + metric_id: sum(metric) / len(metric) + for metric_id, metric in measurements.items() + }) + + # Add metrics of the current script to the result + for metric_id, metric_label in metrics.items(): + if metric_id not in result["metrics"]: + result["metrics"][metric_id] = {"label": metric_label} + else: + # Delete measurements field of the current target + del result["targets"][target_id]["measurements"] + + +def main(): + parser = argparse.ArgumentParser(description="Measurement script for the progress tracker") + + parser.add_argument("base", type=str, help="directory which contains the benchmarks") + parser.add_argument("--output", type=str, help="file which the results will be saved to") + parser.add_argument("--samples", type=int, default=30, help="number of samples to take") + parser.add_argument("--keep", action='store_true', help="flag to keep measurement scripts") + + args = parser.parse_args() + + base = pathlib.Path(args.base) + output = pathlib.Path(args.output) + samples = args.samples + + result = {"metrics": {}, "targets": {}} + scripts = list(base.glob("*.py")) + + # Process each script under the base directory + for script in filter(lambda script: not str(scripts[0]).endswith("measure.py"), scripts): + # Read the script line by line + with open(script, "r") as f: + lines = f.readlines() + + # Find the first non-empty line + first_line = "" + for line in map(lambda line: line.strip(), lines): + if line != "": + first_line = line + break + + # Check whether the script is a target or not + if not first_line.startswith("# Target:"): + print() + print(script) + print("-" * len(str(script))) + + with tqdm.tqdm(total=samples) as pbar: + pbar.write(f" Sample 1") + pbar.write(f" --------") + pbar.write(f" Skipped (doesn't have a `# Target:` directive)\n") + pbar.update(samples) + + print() + continue + + # Extract target name and id + target_name = first_line.replace("# Target:", "").strip() + target_id = name_to_id(target_name) + + # Check whether the target is already registered + if target_id in result["targets"]: + raise RuntimeError(f"Target `{target_name}` is already registered") + + # Create an entry in the result for the current target + result["targets"][target_id] = {"name": target_name, "measurements": {}} + + # Create a dictionary to hold `metric_id` to `metric_name` + metrics = {} + + # Identify metrics of the current script + identify_metrics(script, lines, metrics) + + # Extract the script name without extension + script_without_extension = os.path.splitext(script)[0] + + # Create another script to hold the modified version of the current script + create_modified_script(script_without_extension, lines, metrics) + + # Perform and save measurements + perform_measurements(script, script_without_extension, target_id, metrics, samples, result) + + # Dump the latest results to the output file + with open(output, "w") as f: + json.dump(result, f, indent=2) + + # Delete the modified script if the user doesn't care + if not args.keep: + os.unlink(f"{os.path.splitext(script)[0]}.measure.py") + print() + + +if __name__ == "__main__": + main() From 1e58dde1fa0eb4653cec3c93a77a424315caffe2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 2 Sep 2021 10:37:08 +0200 Subject: [PATCH 0199/1104] tools: improve docker env image size a little by removing apt lists - also update and upgrade packages --- docker/Dockerfile.concretefhe-env | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index 09ca1995b..86d3de0e9 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -1,11 +1,13 @@ FROM ghcr.io/zama-ai/zamalang-compiler -RUN apt-get install --no-install-recommends -y \ +RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ + apt-get install --no-install-recommends -y \ python3.8 \ python3.8-tk \ python3.8-venv \ python-is-python3 \ git \ graphviz* && \ + rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry From a8e7f0e237dcce28ee797abbcbbb50a3c0cee8e3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 2 Sep 2021 10:38:11 +0200 Subject: [PATCH 0200/1104] tools: create Docker release image, helper build script and requirements - also create Documentation and issue template --- .github/ISSUE_TEMPLATE/release.md | 19 ++++++++++ Makefile | 4 +++ docker/Dockerfile.release | 48 ++++++++++++++++++++++++++ docker/Dockerfile.release.dockerignore | 12 +++++++ docker/build_release_image.sh | 5 +++ docker/release_requirements.txt | 4 +++ docs/dev/RELEASING.md | 3 ++ docs/index.rst | 1 + 8 files changed, 96 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/release.md create mode 100644 docker/Dockerfile.release create mode 100644 docker/Dockerfile.release.dockerignore create mode 100644 docker/build_release_image.sh create mode 100644 docker/release_requirements.txt create mode 100644 docs/dev/RELEASING.md diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md new file mode 100644 index 000000000..a9e92e819 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release.md @@ -0,0 +1,19 @@ +--- +name: Release +about: Issue template to prepare a release step by step. +title: "Release vX.Y.Z" +--- + +Release check-list: + +- [ ] Check the release milestone issues, cut out what can't be completed in time +- [ ] Choose the version number following semantic versioning: https://semver.org/ +- [ ] Checkout the commit for release, create a signed tag with the version name `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, push it to GitHub with `git push origin refs/tags/vX.Y.Z` +- [ ] Run sanity checks inside the dev docker: `make pcc` and `make pytest && make coverage` +- [ ] On the build machine with docker installed, run in your OS terminal in the project dir: `make release_docker` +- [ ] Re-tag the image with `docker tag concretefhe-release:latest ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` +- [ ] `docker login ghcr.io`, input your username and GitHub Personal Access Token (PAT). If not already done add `write:packages` to your PAT +- [ ] Push the release image `docker push ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` +- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link \(`ghcr.io/zama-ai/concretefhe-release:vX.Y.Z`\) for the uploaded docker image + +All done! diff --git a/Makefile b/Makefile index f9889d491..297628334 100644 --- a/Makefile +++ b/Makefile @@ -176,3 +176,7 @@ benchmark: jupyter: poetry run jupyter notebook --allow-root --no-browser --ip=0.0.0.0 .PHONY: jupyter + +release_docker: + ./docker/build_release_image.sh +.PHONY: release_docker diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release new file mode 100644 index 000000000..fc072d8e7 --- /dev/null +++ b/docker/Dockerfile.release @@ -0,0 +1,48 @@ +FROM ghcr.io/zama-ai/zamalang-compiler as builder + +RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ + apt-get install --no-install-recommends -y \ + python3.8 \ + python-is-python3 && \ + rm -rf /var/lib/apt/lists/* && \ + python3 -m pip install --no-cache-dir --upgrade pip wheel setuptools && \ + python3 -m pip install --no-cache-dir poetry + +WORKDIR /build +COPY concrete ./concrete +COPY pyproject.toml ./pyproject.toml + +RUN poetry build --format wheel + +FROM ghcr.io/zama-ai/zamalang-compiler + +RUN mkdir /pkg && mkdir /app +WORKDIR /pkg +COPY --from=builder /build/dist/*.whl . +COPY docker/datascience_requirements.txt . +COPY torch_requirements.txt . + +RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ + apt-get install --no-install-recommends -y \ + python3.8 \ + python3.8-tk \ + python-is-python3 \ + graphviz* && \ + rm -rf /var/lib/apt/lists/* && \ + python3 -m pip install --no-cache-dir --upgrade pip wheel setuptools && \ + echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc && \ + echo "export MPLBACKEND=TkAgg" >> /root/.bashrc && \ + python3 -m pip install --no-cache-dir ./*.whl && \ + python3 -m pip install --no-cache-dir -r torch_requirements.txt \ + -f https://download.pytorch.org/whl/torch_stable.html && \ + python3 -m pip install --no-cache-dir -r datascience_requirements.txt + +WORKDIR /app +RUN printf "#!/bin/bash\npython3 -m jupyter notebook --ip=0.0.0.0 --allow-root --no-browser\n" \ + > entry_point.sh && \ + mkdir /data + +WORKDIR /data +VOLUME [ "/data" ] + +CMD ["/bin/bash", "-l", "/app/entry_point.sh"] diff --git a/docker/Dockerfile.release.dockerignore b/docker/Dockerfile.release.dockerignore new file mode 100644 index 000000000..06fbab269 --- /dev/null +++ b/docker/Dockerfile.release.dockerignore @@ -0,0 +1,12 @@ +# Ignore all +** + +# Not our sources +!concrete +!pyproject.toml +!docker/datascience_requirements.txt +!torch_requirements.txt + +# But still ignore pycache +**/__pycache__ +**/*.pyc diff --git a/docker/build_release_image.sh b/docker/build_release_image.sh new file mode 100644 index 000000000..1b0a23683 --- /dev/null +++ b/docker/build_release_image.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +CURR_DIR=$(dirname $0) +DOCKER_BUILDKIT=1 docker build --pull --no-cache -f "$CURR_DIR/Dockerfile.release" \ +-t concretefhe-release "$CURR_DIR/.." diff --git a/docker/release_requirements.txt b/docker/release_requirements.txt new file mode 100644 index 000000000..6f96a3f49 --- /dev/null +++ b/docker/release_requirements.txt @@ -0,0 +1,4 @@ +jupyter~=1.0.0 +opencv-python-headless~=4.5.3.56 +pandas~=1.3.2 +scikit-learn~=0.24.2 diff --git a/docs/dev/RELEASING.md b/docs/dev/RELEASING.md new file mode 100644 index 000000000..a1b631aef --- /dev/null +++ b/docs/dev/RELEASING.md @@ -0,0 +1,3 @@ +# Creating A Release On GitHub + +Please open an issue with the release template: https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md. The check-list will guide you through what's required. diff --git a/docs/index.rst b/docs/index.rst index 7e5a8f13f..9417ec3b6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Homomorphic Development Kit's documentation dev/FLOAT-FUSING.md dev/DOCUMENTING.md dev/CONTRIBUTING.md + dev/RELEASING.md .. toctree:: :maxdepth: 5 From 5041e429785d6b28624b54aa180dd465a76e6cf3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 8 Sep 2021 16:42:19 +0200 Subject: [PATCH 0201/1104] build: test env docker image before pushing as latest - also push with an epoch tag --- .github/workflows/continuous-integration.yaml | 21 ++++- .github/workflows/docker-env.yaml | 80 +++++++++++++++++-- .github/workflows/package-watcher.yaml | 3 +- .../container_timestamp_check.sh | 8 +- 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index a9ffc5ada..6c3a9df84 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -5,15 +5,20 @@ on: branches: - main + # Allows external webhook trigger + repository_dispatch: + types: + - env-docker-preflight + jobs: build: concurrency: - group: ${{ github.ref }} + group: ${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true runs-on: ubuntu-20.04 container: - image: ghcr.io/zama-ai/concretefhe-env + image: ${{ github.event.client_payload.image || 'ghcr.io/zama-ai/concretefhe-env' }} credentials: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} @@ -100,6 +105,18 @@ jobs: with: path: diff-coverage.txt recreate: true + - name: Trigger docker push workflow + if: ${{ github.event_name == 'repository_dispatch' && github.event.event_type == 'env-docker-preflight' }} + env: + WORKFLOW_SUCCESSFUL: toJSON(success()) + run: | + curl \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/dispatches \ + -d '{"event_type":"publish-env-docker"}' \ + -d '{"client_payload": {"preflight_success":"${{ env.WORKFLOW_SUCCESSFUL }}"}}' - name: Slack Notification if: ${{ always() }} continue-on-error: true diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index ad82d803f..cd23ffec4 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -13,18 +13,24 @@ on: # Allows external webhook trigger repository_dispatch: types: - - rebuild-docker + - rebuild-env-docker + - publish-env-docker + +env: + PREFLIGHT_IMAGE: ghcr.io/zama-ai/concretefhe-env:preflight + LATEST_IMAGE: ghcr.io/zama-ai/concretefhe-env:latest + BASE_IMAGE: ghcr.io/zama-ai/concretefhe-env jobs: - build_publish: + build_preflight_docker: + if: ${{ github.event_name != 'repository_dispatch' || github.event.event_type == 'rebuild-env-docker' }} + concurrency: group: ${{ github.ref }} cancel-in-progress: true name: Build & Push the concretefhe env Docker Image runs-on: ubuntu-20.04 - env: - IMAGE_URL: ghcr.io/zama-ai/concretefhe-env steps: - uses: actions/checkout@v2 @@ -46,8 +52,67 @@ jobs: builder: ${{ steps.buildx.outputs.name }} file: docker/Dockerfile.concretefhe-env push: true - tags: "${{ env.IMAGE_URL }}:latest" + tags: "${{ env.PREFLIGHT_IMAGE }}" no-cache: true + - name: Trigger CI pipeline with preflight image + if: ${{ success() && !cancelled() }} + run: | + curl \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/dispatches \ + -d '{"event_type":"env-docker-preflight"}' \ + -d '{"client_payload": {"image":"${{ env.PREFLIGHT_IMAGE }}"}}' + - name: Slack Notification + if: ${{ always() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Docker image preflight build ${{ env.PREFLIGHT_IMAGE }} finished with \ + status ${{ job.status }}" + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + push-docker-image: + if: ${{ github.event_name == 'repository_dispatch' && github.event.event_type == 'publish-env-docker'}} + + concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + + name: Push env docker image + runs-on: ubuntu-20.04 + + steps: + - name: Check build went well with preflight image + env: + CHECKS_OK: ${{ github.event.client_payload.preflight_success }} + run: | + if [[ "${CHECKS_OK}" == "false" ]]; then + echo "Build with new image failed, aborting." + exit 1 + fi + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + - name: Pull preflight image + run: | + docker pull ${PREFLIGHT_IMAGE} + - name: Retag to latest and epoch and push + run: | + EPOCH=$(date +%s) + EPOCH_IMAGE="${BASE_IMAGE}:${EPOCH}" + docker tag ${PREFLIGHT_IMAGE} ${LATEST_IMAGE} + docker tag ${PREFLIGHT_IMAGE} ${EPOCH_IMAGE} + docker push ${LATEST_IMAGE} + docker push ${EPOCH_IMAGE} - name: Slack Notification if: ${{ always() }} @@ -57,8 +122,7 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Publishing Docker Image ${{ env.IMAGE_URL }} \ - finished with status ${{ job.status }}" + SLACK_MESSAGE: "Publishing docker image ${{ env.BASE_IMAGE }} finished with status \ + ${{ job.status }}" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index a14f14044..d0ca3444d 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -22,4 +22,5 @@ jobs: --env_img_url \ https://api.github.com/orgs/zama-ai/packages/container/concretefhe-env/versions \ --token ${{ secrets.BOT_TOKEN }} \ - --org-repo ${{ github.repository }} + --org-repo ${{ github.repository }} \ + --event-type rebuild-env-docker diff --git a/script/actions_utils/container_timestamp_check.sh b/script/actions_utils/container_timestamp_check.sh index 3c52a1b5d..902171e7f 100755 --- a/script/actions_utils/container_timestamp_check.sh +++ b/script/actions_utils/container_timestamp_check.sh @@ -6,6 +6,7 @@ BASE_IMG_ENDPOINT_URL= ENV_IMG_ENDPOINT_URL= TOKEN= ORG_REPO= +EVENT_TYPE= while [ -n "$1" ] do @@ -30,6 +31,11 @@ do ORG_REPO="$1" ;; + "--event-type" ) + shift + EVENT_TYPE="$1" + ;; + *) echo "Unknown param : $1" exit -1 @@ -70,7 +76,7 @@ if [[ "${BASE_IMG_DATE}" -ge "${ENV_IMG_DATE}" ]]; then -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${TOKEN}" \ https://api.github.com/repos/${ORG_REPO}/dispatches \ - -d '{"event_type":"rebuild-docker"}' + -d "{\"event_type\":\"${EVENT_TYPE}\"}" else echo "Image up to date, nothing to do." fi From bcc146bd6ebf16795eceae127deb21786b461cd2 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 8 Sep 2021 11:38:49 +0200 Subject: [PATCH 0202/1104] doc: let's agree on a plan closes #305 --- .github/ISSUE_TEMPLATE/refactor.md | 2 +- README.md | 2 +- docs/README.md | 3 ++ docs/dev/{ => explanation}/COMPILATION.md | 10 ++-- docs/dev/{ => explanation}/FLOAT-FUSING.md | 6 +-- docs/dev/explanation/MLIR.md | 4 ++ .../TERMINOLOGY_AND_STRUCTURE.md | 0 docs/dev/{ => howto}/CONTRIBUTING.md | 2 +- docs/dev/{ => howto}/DOCUMENTING.md | 0 docs/dev/{ => howto}/RELEASING.md | 0 docs/index.rst | 54 ++++++++++++++----- docs/user/FIRST_USE.md | 3 -- docs/user/explanation/FHE_LIMITS.md | 2 + docs/user/explanation/FUTURE_FEATURES.md | 2 + docs/user/explanation/QUANTIZATION.md | 2 + docs/user/explanation/WHAT_IS_FHE.md | 1 + docs/user/howto/COMPILE.md | 1 + docs/user/howto/DEBUG.md | 1 + docs/user/howto/FAQ.md | 3 ++ docs/user/howto/REDUCE_NEEDED_PRECISION.md | 1 + docs/user/howto/SUBMIT_ISSUE.md | 1 + docs/user/howto/SUPPORT.md | 1 + docs/user/tutorial/FIRST_TUTORIAL.md | 3 ++ docs/user/tutorial/SECOND_TUTORIAL.md | 3 ++ docs/user/tutorial/THIRD_TUTORIAL.md | 3 ++ 25 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 docs/README.md rename docs/dev/{ => explanation}/COMPILATION.md (96%) rename docs/dev/{ => explanation}/FLOAT-FUSING.md (96%) create mode 100644 docs/dev/explanation/MLIR.md rename docs/dev/{ => explanation}/TERMINOLOGY_AND_STRUCTURE.md (100%) rename docs/dev/{ => howto}/CONTRIBUTING.md (98%) rename docs/dev/{ => howto}/DOCUMENTING.md (100%) rename docs/dev/{ => howto}/RELEASING.md (100%) delete mode 100644 docs/user/FIRST_USE.md create mode 100644 docs/user/explanation/FHE_LIMITS.md create mode 100644 docs/user/explanation/FUTURE_FEATURES.md create mode 100644 docs/user/explanation/QUANTIZATION.md create mode 100644 docs/user/explanation/WHAT_IS_FHE.md create mode 100644 docs/user/howto/COMPILE.md create mode 100644 docs/user/howto/DEBUG.md create mode 100644 docs/user/howto/FAQ.md create mode 100644 docs/user/howto/REDUCE_NEEDED_PRECISION.md create mode 100644 docs/user/howto/SUBMIT_ISSUE.md create mode 100644 docs/user/howto/SUPPORT.md create mode 100644 docs/user/tutorial/FIRST_TUTORIAL.md create mode 100644 docs/user/tutorial/SECOND_TUTORIAL.md create mode 100644 docs/user/tutorial/THIRD_TUTORIAL.md diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md index dc8d53030..0084792d9 100644 --- a/.github/ISSUE_TEMPLATE/refactor.md +++ b/.github/ISSUE_TEMPLATE/refactor.md @@ -16,4 +16,4 @@ List all files/modules/projects impacted by this refactor.

file.py

-
\ No newline at end of file +
diff --git a/README.md b/README.md index b31760944..1134bb33f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Concrete Framework Python API - collection of tools to FHE all the things -## Installing +## Installing Installation steps are described in [INSTALLING.md](docs/install/INSTALLING.md). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..dc8e892b4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# What is ConcreteFHE + +To be done diff --git a/docs/dev/COMPILATION.md b/docs/dev/explanation/COMPILATION.md similarity index 96% rename from docs/dev/COMPILATION.md rename to docs/dev/explanation/COMPILATION.md index 32b8ca63e..5b9dc9321 100644 --- a/docs/dev/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -54,7 +54,7 @@ Once the MLIR is prepared, the rest of the stack, which you can learn more about Here is the visual representation of the pipeline: -![Frontend Flow](../_static/compilation-pipeline/frontend_flow.svg) +![Frontend Flow](../../_static/compilation-pipeline/frontend_flow.svg) ## Tracing @@ -67,7 +67,7 @@ def f(x): the goal of tracing is to create the following operation graph without needing any change from the user. -![](../_static/compilation-pipeline/two_x_plus_three.png) +![](../../_static/compilation-pipeline/two_x_plus_three.png) (Note that the edge labels are for non-commutative operations. To give an example, a subtraction node represents `(predecessor with edge label 0) - (predecessor with edge label 1)`) @@ -140,7 +140,7 @@ After the entire dataset is evaluated, we assign a data type to each node using Here is an example, given this operation graph where `x` is encrypted: -![](../_static/compilation-pipeline/two_x_plus_three.png) +![](../../_static/compilation-pipeline/two_x_plus_three.png) and this dataset: @@ -218,7 +218,7 @@ x = EncryptedScalar(UnsignedInteger(2)) #### Corresponding Operation Graph -![](../_static/compilation-pipeline/two_x_plus_three.png) +![](../../_static/compilation-pipeline/two_x_plus_three.png) ### Topological Transforms @@ -268,7 +268,7 @@ y = EncryptedScalar(UnsignedInteger(1)) #### Corresponding Operation Graph -![](../_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png) +![](../../_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png) ### Topological Transforms diff --git a/docs/dev/FLOAT-FUSING.md b/docs/dev/explanation/FLOAT-FUSING.md similarity index 96% rename from docs/dev/FLOAT-FUSING.md rename to docs/dev/explanation/FLOAT-FUSING.md index b0c833b0d..a283764e9 100644 --- a/docs/dev/FLOAT-FUSING.md +++ b/docs/dev/explanation/FLOAT-FUSING.md @@ -24,15 +24,15 @@ Any computation where there is a single variable integer input and a single inte The `quantized_sin` graph of operations: -![](../_static/float_fusing_example/before.png) +![](../../_static/float_fusing_example/before.png) The float subgraph that was detected: -![](../_static/float_fusing_example/subgraph.png) +![](../../_static/float_fusing_example/subgraph.png) The simplified graph of operations with the float subgraph condensed in an `ArbitraryFunction` node: -![](../_static/float_fusing_example/after.png) +![](../../_static/float_fusing_example/after.png) ## How is it done in concretefhe? diff --git a/docs/dev/explanation/MLIR.md b/docs/dev/explanation/MLIR.md new file mode 100644 index 000000000..74c37dea7 --- /dev/null +++ b/docs/dev/explanation/MLIR.md @@ -0,0 +1,4 @@ +# MLIR + +to be done + diff --git a/docs/dev/TERMINOLOGY_AND_STRUCTURE.md b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md similarity index 100% rename from docs/dev/TERMINOLOGY_AND_STRUCTURE.md rename to docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md diff --git a/docs/dev/CONTRIBUTING.md b/docs/dev/howto/CONTRIBUTING.md similarity index 98% rename from docs/dev/CONTRIBUTING.md rename to docs/dev/howto/CONTRIBUTING.md index 713d73031..d4c401488 100644 --- a/docs/dev/CONTRIBUTING.md +++ b/docs/dev/howto/CONTRIBUTING.md @@ -95,7 +95,7 @@ git checkout $YOUR_BRANCH # rebase on top of main branch git rebase main -# push the latest version of the local branch to remote +# push the latest version of the local branch to remote git push --force ``` diff --git a/docs/dev/DOCUMENTING.md b/docs/dev/howto/DOCUMENTING.md similarity index 100% rename from docs/dev/DOCUMENTING.md rename to docs/dev/howto/DOCUMENTING.md diff --git a/docs/dev/RELEASING.md b/docs/dev/howto/RELEASING.md similarity index 100% rename from docs/dev/RELEASING.md rename to docs/dev/howto/RELEASING.md diff --git a/docs/index.rst b/docs/index.rst index 9417ec3b6..fcbe69845 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,30 +4,60 @@ Homomorphic Development Kit's documentation .. toctree:: :maxdepth: 2 - :caption: Installing + :caption: Basics + README.md install/INSTALLING.md install/DOCKER.md .. toctree:: :maxdepth: 2 - :caption: User doc + :caption: Tutorial - user/FIRST_USE.md + user/tutorial/FIRST_TUTORIAL.md + user/tutorial/SECOND_TUTORIAL.md + user/tutorial/THIRD_TUTORIAL.md .. toctree:: :maxdepth: 2 - :caption: Developer doc + :caption: How to - dev/TERMINOLOGY_AND_STRUCTURE.md - dev/COMPILATION.md - dev/FLOAT-FUSING.md - dev/DOCUMENTING.md - dev/CONTRIBUTING.md - dev/RELEASING.md + user/howto/COMPILE.md + user/howto/REDUCE_NEEDED_PRECISION.md + user/howto/DEBUG.md + user/howto/SUBMIT_ISSUE.md + user/howto/SUPPORT.md + user/howto/FAQ.md + +.. toctree:: + :maxdepth: 2 + :caption: Explanation + + user/explanation/WHAT_IS_FHE.md + user/explanation/FHE_LIMITS.md + user/explanation/QUANTIZATION.md + user/explanation/FUTURE_FEATURES.md .. toctree:: :maxdepth: 5 - :caption: Docs from sources + :caption: Reference - _apidoc/modules.rst + API <_apidoc/modules.rst> + +.. toctree:: + :maxdepth: 2 + :caption: Developper - How To + + dev/howto/DOCUMENTING.md + Releasing on GitHub + dev/howto/CONTRIBUTING.md + +.. toctree:: + :maxdepth: 2 + :caption: Developper - Explanation + + dev/explanation/COMPILATION.md + dev/explanation/TERMINOLOGY_AND_STRUCTURE.md + dev/explanation/FLOAT-FUSING.md + dev/explanation/MLIR.md + \ No newline at end of file diff --git a/docs/user/FIRST_USE.md b/docs/user/FIRST_USE.md deleted file mode 100644 index 186e1f055..000000000 --- a/docs/user/FIRST_USE.md +++ /dev/null @@ -1,3 +0,0 @@ -# First Use - -To be continued \ No newline at end of file diff --git a/docs/user/explanation/FHE_LIMITS.md b/docs/user/explanation/FHE_LIMITS.md new file mode 100644 index 000000000..df9defe8c --- /dev/null +++ b/docs/user/explanation/FHE_LIMITS.md @@ -0,0 +1,2 @@ +# FHE Limits + diff --git a/docs/user/explanation/FUTURE_FEATURES.md b/docs/user/explanation/FUTURE_FEATURES.md new file mode 100644 index 000000000..2ee513008 --- /dev/null +++ b/docs/user/explanation/FUTURE_FEATURES.md @@ -0,0 +1,2 @@ +# Future Features + diff --git a/docs/user/explanation/QUANTIZATION.md b/docs/user/explanation/QUANTIZATION.md new file mode 100644 index 000000000..7ab168d56 --- /dev/null +++ b/docs/user/explanation/QUANTIZATION.md @@ -0,0 +1,2 @@ +# Quantization + diff --git a/docs/user/explanation/WHAT_IS_FHE.md b/docs/user/explanation/WHAT_IS_FHE.md new file mode 100644 index 000000000..ce4af9382 --- /dev/null +++ b/docs/user/explanation/WHAT_IS_FHE.md @@ -0,0 +1 @@ +# What is FHE? diff --git a/docs/user/howto/COMPILE.md b/docs/user/howto/COMPILE.md new file mode 100644 index 000000000..6d6a98c48 --- /dev/null +++ b/docs/user/howto/COMPILE.md @@ -0,0 +1 @@ +# Compiling diff --git a/docs/user/howto/DEBUG.md b/docs/user/howto/DEBUG.md new file mode 100644 index 000000000..1e584c954 --- /dev/null +++ b/docs/user/howto/DEBUG.md @@ -0,0 +1 @@ +# Debugging diff --git a/docs/user/howto/FAQ.md b/docs/user/howto/FAQ.md new file mode 100644 index 000000000..9a2b02bd1 --- /dev/null +++ b/docs/user/howto/FAQ.md @@ -0,0 +1,3 @@ +# FAQ + +to be done diff --git a/docs/user/howto/REDUCE_NEEDED_PRECISION.md b/docs/user/howto/REDUCE_NEEDED_PRECISION.md new file mode 100644 index 000000000..6877dc089 --- /dev/null +++ b/docs/user/howto/REDUCE_NEEDED_PRECISION.md @@ -0,0 +1 @@ +# Having a Function Which Requires Less Precision diff --git a/docs/user/howto/SUBMIT_ISSUE.md b/docs/user/howto/SUBMIT_ISSUE.md new file mode 100644 index 000000000..3e72eafda --- /dev/null +++ b/docs/user/howto/SUBMIT_ISSUE.md @@ -0,0 +1 @@ +# Submitting Issues diff --git a/docs/user/howto/SUPPORT.md b/docs/user/howto/SUPPORT.md new file mode 100644 index 000000000..85d1c2942 --- /dev/null +++ b/docs/user/howto/SUPPORT.md @@ -0,0 +1 @@ +# Support diff --git a/docs/user/tutorial/FIRST_TUTORIAL.md b/docs/user/tutorial/FIRST_TUTORIAL.md new file mode 100644 index 000000000..6c1c7b9bc --- /dev/null +++ b/docs/user/tutorial/FIRST_TUTORIAL.md @@ -0,0 +1,3 @@ +# First Tutorial + +To be continued diff --git a/docs/user/tutorial/SECOND_TUTORIAL.md b/docs/user/tutorial/SECOND_TUTORIAL.md new file mode 100644 index 000000000..9167da9e6 --- /dev/null +++ b/docs/user/tutorial/SECOND_TUTORIAL.md @@ -0,0 +1,3 @@ +# Second Tutorial + +To be continued diff --git a/docs/user/tutorial/THIRD_TUTORIAL.md b/docs/user/tutorial/THIRD_TUTORIAL.md new file mode 100644 index 000000000..44fd7293d --- /dev/null +++ b/docs/user/tutorial/THIRD_TUTORIAL.md @@ -0,0 +1,3 @@ +# Third Tutorial + +To be continued From fcaa141d2e4fabc289759ad2482798dba199003c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 11:04:32 +0200 Subject: [PATCH 0203/1104] chore: update release issue template - indicate where to update the python package version --- .github/ISSUE_TEMPLATE/release.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index a9e92e819..56f7465b0 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -7,7 +7,8 @@ title: "Release vX.Y.Z" Release check-list: - [ ] Check the release milestone issues, cut out what can't be completed in time -- [ ] Choose the version number following semantic versioning: https://semver.org/ +- [ ] Choose the version number, e.g. `vX.Y.Z` following semantic versioning: https://semver.org/ +- [ ] Update the version in pyproject.toml to `X.Y.Z` - [ ] Checkout the commit for release, create a signed tag with the version name `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, push it to GitHub with `git push origin refs/tags/vX.Y.Z` - [ ] Run sanity checks inside the dev docker: `make pcc` and `make pytest && make coverage` - [ ] On the build machine with docker installed, run in your OS terminal in the project dir: `make release_docker` From e13347affc2363fcdc54351efac0f767bae00a86 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 09:51:07 +0200 Subject: [PATCH 0204/1104] fix(build): fix curl data and gha statuses/expressions --- .github/workflows/continuous-integration.yaml | 7 ++----- .github/workflows/docker-env.yaml | 7 +++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 6c3a9df84..6d6d55ee1 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -106,17 +106,14 @@ jobs: path: diff-coverage.txt recreate: true - name: Trigger docker push workflow - if: ${{ github.event_name == 'repository_dispatch' && github.event.event_type == 'env-docker-preflight' }} - env: - WORKFLOW_SUCCESSFUL: toJSON(success()) + if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.event_type == 'env-docker-preflight' }} run: | curl \ -X POST \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ https://api.github.com/repos/${{ github.repository }}/dispatches \ - -d '{"event_type":"publish-env-docker"}' \ - -d '{"client_payload": {"preflight_success":"${{ env.WORKFLOW_SUCCESSFUL }}"}}' + -d '{"event_type":"publish-env-docker","client_payload":{"preflight_status":"${{ job.status }}"}}' - name: Slack Notification if: ${{ always() }} continue-on-error: true diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index cd23ffec4..7632a18c1 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -62,8 +62,7 @@ jobs: -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ https://api.github.com/repos/${{ github.repository }}/dispatches \ - -d '{"event_type":"env-docker-preflight"}' \ - -d '{"client_payload": {"image":"${{ env.PREFLIGHT_IMAGE }}"}}' + -d '{"event_type":"env-docker-preflight","client_payload":{"image":"${{ env.PREFLIGHT_IMAGE }}"}}' - name: Slack Notification if: ${{ always() }} continue-on-error: true @@ -90,9 +89,9 @@ jobs: steps: - name: Check build went well with preflight image env: - CHECKS_OK: ${{ github.event.client_payload.preflight_success }} + PREFLIGHT_STATUS: ${{ github.event.client_payload.preflight_status }} run: | - if [[ "${CHECKS_OK}" == "false" ]]; then + if [[ "${PREFLIGHT_STATUS}" != "success" ]]; then echo "Build with new image failed, aborting." exit 1 fi From 269ce01db31323bc745dcd2d5a55bb01ca93e173 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 8 Sep 2021 11:56:31 +0200 Subject: [PATCH 0205/1104] chore(req): force python version to 3.8 - compiler bindings only support 3.8 - update requirements --- poetry.lock | 134 +++++++++---------------------------------------- pyproject.toml | 2 +- 2 files changed, 24 insertions(+), 112 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2bc3c6302..f0b5b37ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -48,8 +48,6 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" -typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} wrapt = ">=1.11,<1.13" [[package]] @@ -108,8 +106,6 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.8.1,<1" regex = ">=2020.1.8" tomli = ">=0.2.6,<2.0.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -178,7 +174,6 @@ python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -228,18 +223,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.2.1" -description = "Automatically find diff lines that need test coverage." +version = "6.3.5" +description = "Run coverage and linting reports on diffs" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.6.2,<4.0.0" [package.dependencies] chardet = ">=3.0.0" Jinja2 = ">=2.7.1" -jinja2-pluralize = "*" -pluggy = "*" -pygments = "*" +jinja2_pluralize = ">=0.3.0,<0.4.0" +pluggy = ">=0.13.1,<0.14.0" +Pygments = ">=2.9.0,<3.0.0" + +[package.extras] +toml = ["tomli (>=1.2.1,<2.0.0)"] [[package]] name = "docutils" @@ -266,7 +264,6 @@ optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" @@ -302,23 +299,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "importlib-metadata" -version = "4.8.1" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - [[package]] name = "inflect" version = "5.3.0" @@ -482,7 +462,6 @@ python-versions = "*" [package.dependencies] attrs = ">=17.4.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} pyrsistent = ">=0.14.0" six = ">=1.11.0" @@ -602,7 +581,6 @@ python-versions = "~=3.6" [package.dependencies] attrs = ">=19,<22" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] code_style = ["pre-commit (==2.6)"] @@ -638,7 +616,7 @@ python-dateutil = ">=2.7" [[package]] name = "matplotlib-inline" -version = "0.1.2" +version = "0.1.3" description = "Inline Matplotlib backend for Jupyter" category = "dev" optional = false @@ -690,7 +668,6 @@ python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" toml = "*" -typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} typing-extensions = ">=3.7.4" [package.extras] @@ -959,18 +936,14 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock [[package]] name = "pluggy" -version = "1.0.0" +version = "0.13.1" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" @@ -1123,7 +1096,6 @@ python-versions = ">=3.6" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -1408,7 +1380,7 @@ test = ["pytest"] [[package]] name = "terminado" -version = "0.11.1" +version = "0.12.1" description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." category = "dev" optional = false @@ -1484,14 +1456,6 @@ python-versions = ">=3.7" [package.extras] test = ["pytest"] -[[package]] -name = "typed-ast" -version = "1.4.3" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "typing-extensions" version = "3.10.0.2" @@ -1548,22 +1512,10 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "zipp" -version = "3.5.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - [metadata] lock-version = "1.1" -python-versions = ">=3.7,<3.10" -content-hash = "bc8dd9b8a8b3320ee517534a31a18bc11675799ccf6f0d2972c1d283ac50b720" +python-versions = ">=3.8,<3.9" +content-hash = "cb1c1db2c4f94ed4984e565d1b5c0633bcf58a57cd667ff09a18e01e73382aa4" [metadata.files] alabaster = [ @@ -1753,8 +1705,8 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.2.1-py3-none-any.whl", hash = "sha256:cfe6333c9b83a28f91214e06e3a86108aeeb6a9a31233944ce8ef236121ba899"}, - {file = "diff_cover-6.2.1.tar.gz", hash = "sha256:b22fe97d118fadb2528bbc0a1f9fc370491b29b1b3fe2e2f1cdb94bf028814c7"}, + {file = "diff_cover-6.3.5-py3-none-any.whl", hash = "sha256:d8f0949c8a57f7ed4b93d0012fbe9e77898571070cd35bd33e29d0c4b2045b0d"}, + {file = "diff_cover-6.3.5.tar.gz", hash = "sha256:53b90abb1d33ef0361d15c8761d260f572b981b82a104dfb70de67a461d8cd42"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1780,10 +1732,6 @@ imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] -importlib-metadata = [ - {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, - {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, -] inflect = [ {file = "inflect-5.3.0-py3-none-any.whl", hash = "sha256:42560be16af702a21d43d59427f276b5aed79efb1ded9b713468c081f4353d10"}, {file = "inflect-5.3.0.tar.gz", hash = "sha256:41a23f6788962e9775e40e2ecfb1d6455d02de315022afeedd3c5dc070019d73"}, @@ -2007,8 +1955,8 @@ matplotlib = [ {file = "matplotlib-3.4.3.tar.gz", hash = "sha256:fc4f526dfdb31c9bd6b8ca06bf9fab663ca12f3ec9cdf4496fb44bc680140318"}, ] matplotlib-inline = [ - {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, - {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, + {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, + {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -2201,8 +2149,8 @@ platformdirs = [ {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, ] pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] prometheus-client = [ {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"}, @@ -2503,8 +2451,8 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] terminado = [ - {file = "terminado-0.11.1-py3-none-any.whl", hash = "sha256:9e0457334863be3e6060c487ad60e0995fa1df54f109c67b24ff49a4f2f34df5"}, - {file = "terminado-0.11.1.tar.gz", hash = "sha256:962b402edbb480718054dc37027bada293972ecadfb587b89f01e2b8660a2132"}, + {file = "terminado-0.12.1-py3-none-any.whl", hash = "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452"}, + {file = "terminado-0.12.1.tar.gz", hash = "sha256:b20fd93cc57c1678c799799d117874367cc07a3d2d55be95205b1a88fa08393f"}, ] testpath = [ {file = "testpath-0.5.0-py3-none-any.whl", hash = "sha256:8044f9a0bab6567fc644a3593164e872543bb44225b0e24846e2c89237937589"}, @@ -2569,38 +2517,6 @@ traitlets = [ {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, ] -typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, -] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, @@ -2625,7 +2541,3 @@ widgetsnbextension = [ wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] -zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, -] diff --git a/pyproject.toml b/pyproject.toml index e544028f9..4459463e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.7,<3.10" +python = ">=3.8,<3.9" networkx = "^2.6.1" matplotlib = "^3.4.2" numpy = "^1.21.1" From 6fe809aeced1e7ff65243aeead6e9c580bb4f2be Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 6 Sep 2021 17:32:10 +0200 Subject: [PATCH 0206/1104] dev: add code to have the proper data type when getting an AF table - update BaseDataType to store the underlying type constructor e.g. int - add helper functions to get the type constructor for constant data - update operator graph to fill the type constructor during bounds update - add loguru as logger - use type constructor in ArbitraryFunction.get_table, log an info if the type_constructor of the input was None and default to int --- concrete/common/data_types/base.py | 8 ++++ concrete/common/data_types/dtypes_helpers.py | 9 +++++ concrete/common/data_types/floats.py | 1 + concrete/common/data_types/integers.py | 1 + concrete/common/operator_graph.py | 40 ++++++++++++++++--- .../common/representation/intermediate.py | 13 +++++- concrete/numpy/compile.py | 9 ++++- concrete/numpy/np_dtypes_helpers.py | 24 ++++++++++- poetry.lock | 38 +++++++++++++++++- pyproject.toml | 1 + tests/numpy/test_np_dtypes_helpers.py | 21 ++++++++++ 11 files changed, 154 insertions(+), 11 deletions(-) diff --git a/concrete/common/data_types/base.py b/concrete/common/data_types/base.py index 834e75dc9..dec328fb3 100644 --- a/concrete/common/data_types/base.py +++ b/concrete/common/data_types/base.py @@ -1,11 +1,19 @@ """File holding code to represent data types in a program.""" from abc import ABC, abstractmethod +from typing import Optional, Type class BaseDataType(ABC): """Base class to represent a data type.""" + # Constructor for the data type represented (for example numpy.int32 for an int32 numpy array) + underlying_type_constructor: Optional[Type] + + def __init__(self) -> None: + super().__init__() + self.underlying_type_constructor = None + @abstractmethod def __eq__(self, o: object) -> bool: """No default implementation.""" diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 7234c311f..83b4a5084 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -336,3 +336,12 @@ def get_base_value_for_python_constant_data( """ constant_data_type = get_base_data_type_for_python_constant_data(constant_data) return partial(ScalarValue, data_type=constant_data_type) + + +def get_type_constructor_for_python_constant_data(constant_data: Union[int, float]): + """Get the constructor for the passed python constant data. + + Args: + constant_data (Any): The data for which we want to determine the type constructor. + """ + return type(constant_data) diff --git a/concrete/common/data_types/floats.py b/concrete/common/data_types/floats.py index 9161bb391..63b52b3b3 100644 --- a/concrete/common/data_types/floats.py +++ b/concrete/common/data_types/floats.py @@ -13,6 +13,7 @@ class Float(base.BaseDataType): bit_width: int def __init__(self, bit_width: int) -> None: + super().__init__() assert bit_width in (32, 64), "Only 32 and 64 bits floats are supported" self.bit_width = bit_width diff --git a/concrete/common/data_types/integers.py b/concrete/common/data_types/integers.py index 7ef0674d7..2cbe83560 100644 --- a/concrete/common/data_types/integers.py +++ b/concrete/common/data_types/integers.py @@ -13,6 +13,7 @@ class Integer(base.BaseDataType): is_signed: bool def __init__(self, bit_width: int, is_signed: bool) -> None: + super().__init__() assert bit_width > 0, "bit_width must be > 0" self.bit_width = bit_width self.is_signed = is_signed diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index 313e25ec1..b19c42140 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -1,12 +1,15 @@ """Code to wrap and make manipulating networkx graphs easier.""" from copy import deepcopy -from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Type, Union import networkx as nx from .data_types.base import BaseDataType -from .data_types.dtypes_helpers import get_base_data_type_for_python_constant_data +from .data_types.dtypes_helpers import ( + get_base_data_type_for_python_constant_data, + get_type_constructor_for_python_constant_data, +) from .data_types.floats import Float from .data_types.integers import Integer, make_integer_to_hold from .representation import intermediate as ir @@ -124,8 +127,9 @@ class OPGraph: curr_inputs = {} for pred_node in self.graph.pred[node]: edges = self.graph.get_edge_data(pred_node, node) - for edge in edges.values(): - curr_inputs[edge["input_idx"]] = node_results[pred_node] + curr_inputs.update( + {edge["input_idx"]: node_results[pred_node] for edge in edges.values()} + ) node_results[node] = node.evaluate(curr_inputs) else: node_results[node] = node.evaluate({0: inputs[node.program_input_idx]}) @@ -138,6 +142,9 @@ class OPGraph: get_base_data_type_for_constant_data: Callable[ [Any], BaseDataType ] = get_base_data_type_for_python_constant_data, + get_type_constructor_for_constant_data: Callable[ + ..., Type + ] = get_type_constructor_for_python_constant_data, ): """Update values with bounds. @@ -147,10 +154,13 @@ class OPGraph: Args: node_bounds (dict): Dictionary with nodes as keys, holding dicts with a 'min' and 'max' keys. Those bounds will be taken as the data range to be represented, per node. - get_base_data_type_for_constant_data (Callable[ [Type], BaseDataType ], optional): This + get_base_data_type_for_constant_data (Callable[ [Any], BaseDataType ], optional): This is a callback function to convert data encountered during value updates to BaseDataType. This allows to manage data coming from foreign frameworks without specialising OPGraph. Defaults to get_base_data_type_for_python_constant_data. + get_type_constructor_for_constant_data (Callable[ ..., Type ], optional): This is a + callback function to determine the type constructor of the data encountered while + updating the graph bounds. Defaults to get_type_constructor_python_constant_data. """ node: ir.IntermediateNode @@ -164,6 +174,16 @@ class OPGraph: min_data_type = get_base_data_type_for_constant_data(min_bound) max_data_type = get_base_data_type_for_constant_data(max_bound) + min_data_type_constructor = get_type_constructor_for_constant_data(min_bound) + max_data_type_constructor = get_type_constructor_for_constant_data(max_bound) + + assert max_data_type_constructor == min_data_type_constructor, ( + f"Got two different type constructors for min and max bound: " + f"{min_data_type_constructor}, {max_data_type_constructor}" + ) + + data_type_constructor = max_data_type_constructor + if not isinstance(node, ir.Input): for output_value in node.outputs: if isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer): @@ -171,7 +191,15 @@ class OPGraph: (min_bound, max_bound), force_signed=False ) else: + assert isinstance(min_data_type, Float) and isinstance( + max_data_type, Float + ), ( + "min_bound and max_bound have different common types, " + "this should never happen.\n" + f"min_bound: {min_data_type}, max_bound: {max_data_type}" + ) output_value.data_type = Float(64) + output_value.data_type.underlying_type_constructor = data_type_constructor else: # Currently variable inputs are only allowed to be integers assert isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer), ( @@ -181,6 +209,8 @@ class OPGraph: node.inputs[0].data_type = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) + node.inputs[0].data_type.underlying_type_constructor = data_type_constructor + node.outputs[0] = deepcopy(node.inputs[0]) # TODO: #57 manage multiple outputs from a node, probably requires an output_idx when diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index 5f4ba2d23..ed6325510 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -4,6 +4,8 @@ from abc import ABC, abstractmethod from copy import deepcopy from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type +from loguru import logger + from ..data_types.base import BaseDataType from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, @@ -232,6 +234,8 @@ class ArbitraryFunction(IntermediateNode): def get_table(self) -> List[Any]: """Get the table for the current input value of this ArbitraryFunction. + This function only works if the ArbitraryFunction input value is an unsigned Integer. + Returns: List[Any]: The table. """ @@ -243,11 +247,18 @@ class ArbitraryFunction(IntermediateNode): 0 ].data_type.is_signed, "get_table only works for an unsigned Integer input" + type_constructor = self.inputs[0].data_type.underlying_type_constructor + if type_constructor is None: + logger.info( + f"{self.__class__.__name__} input data type constructor was None, defaulting to int" + ) + type_constructor = int + min_input_range = self.inputs[0].data_type.min_value() max_input_range = self.inputs[0].data_type.max_value() + 1 table = [ - self.evaluate({0: input_value}) + self.evaluate({0: type_constructor(input_value)}) for input_value in range(min_input_range, max_input_range) ] diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 8ac26c9e5..333e73d10 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -19,7 +19,10 @@ from ..common.optimization.topological import fuse_float_operations from ..common.representation import intermediate as ir from ..common.values import BaseValue from ..numpy.tracing import trace_numpy_function -from .np_dtypes_helpers import get_base_data_type_for_numpy_or_python_constant_data +from .np_dtypes_helpers import ( + get_base_data_type_for_numpy_or_python_constant_data, + get_type_constructor_for_numpy_or_python_constant_data, +) def numpy_max_func(lhs: Any, rhs: Any) -> Any: @@ -115,7 +118,9 @@ def _compile_numpy_function_into_op_graph_internal( # Update the graph accordingly: after that, we have the compilable graph op_graph.update_values_with_bounds( - node_bounds, get_base_data_type_for_numpy_or_python_constant_data + node_bounds, + get_base_data_type_for_numpy_or_python_constant_data, + get_type_constructor_for_numpy_or_python_constant_data, ) # Add the initial graph as an artifact diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 69586c1cc..1158925c4 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List, Type, Union import numpy from numpy.typing import DTypeLike @@ -12,6 +12,7 @@ from ..common.data_types.dtypes_helpers import ( BASE_DATA_TYPES, get_base_data_type_for_python_constant_data, get_base_value_for_python_constant_data, + get_type_constructor_for_python_constant_data, ) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer @@ -193,3 +194,24 @@ def get_numpy_function_output_dtype( numpy.seterr(**old_numpy_err_settings) return [output.dtype for output in outputs] + + +def get_type_constructor_for_numpy_or_python_constant_data(constant_data: Any): + """Get the constructor for the numpy scalar underlying dtype or python dtype. + + Args: + constant_data (Any): The data for which we want to determine the type constructor. + """ + + assert isinstance( + constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) + ), f"Unsupported constant data of type {type(constant_data)}" + + scalar_constructor: Type + + if isinstance(constant_data, (numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)): + scalar_constructor = constant_data.dtype.type + else: + scalar_constructor = get_type_constructor_for_python_constant_data(constant_data) + + return scalar_constructor diff --git a/poetry.lock b/poetry.lock index f0b5b37ec..bf25a86d8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -179,7 +179,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -571,6 +571,21 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +[[package]] +name = "loguru" +version = "0.5.3" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"] + [[package]] name = "markdown-it-py" version = "1.1.0" @@ -1504,6 +1519,17 @@ python-versions = "*" [package.dependencies] notebook = ">=4.4.1" +[[package]] +name = "win32-setctime" +version = "1.0.3" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] + [[package]] name = "wrapt" version = "1.12.1" @@ -1515,7 +1541,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "cb1c1db2c4f94ed4984e565d1b5c0633bcf58a57cd667ff09a18e01e73382aa4" +content-hash = "8a3be3fe122eddfb9a28a4f789c4581311ada706406277b5e84beb431369163b" [metadata.files] alabaster = [ @@ -1871,6 +1897,10 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, ] +loguru = [ + {file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"}, + {file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"}, +] markdown-it-py = [ {file = "markdown-it-py-1.1.0.tar.gz", hash = "sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3"}, {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, @@ -2538,6 +2568,10 @@ widgetsnbextension = [ {file = "widgetsnbextension-3.5.1-py2.py3-none-any.whl", hash = "sha256:bd314f8ceb488571a5ffea6cc5b9fc6cba0adaf88a9d2386b93a489751938bcd"}, {file = "widgetsnbextension-3.5.1.tar.gz", hash = "sha256:079f87d87270bce047512400efd70238820751a11d2d8cb137a5a5bdbaf255c7"}, ] +win32-setctime = [ + {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"}, + {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, +] wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] diff --git a/pyproject.toml b/pyproject.toml index 4459463e6..cc9d9c71b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ matplotlib = "^3.4.2" numpy = "^1.21.1" pygraphviz = "^1.7" Pillow = "^8.3.1" +loguru = "^0.5.3" [tool.poetry.dev-dependencies] isort = "^5.9.2" diff --git a/tests/numpy/test_np_dtypes_helpers.py b/tests/numpy/test_np_dtypes_helpers.py index 6961c714f..d48180657 100644 --- a/tests/numpy/test_np_dtypes_helpers.py +++ b/tests/numpy/test_np_dtypes_helpers.py @@ -8,6 +8,7 @@ from concrete.common.data_types.integers import Integer from concrete.numpy.np_dtypes_helpers import ( convert_base_data_type_to_numpy_dtype, convert_numpy_dtype_to_base_data_type, + get_type_constructor_for_numpy_or_python_constant_data, ) @@ -55,3 +56,23 @@ def test_convert_numpy_dtype_to_base_data_type(numpy_dtype, expected_common_type def test_convert_common_dtype_to_numpy_dtype(common_dtype, expected_numpy_dtype): """Test function for convert_common_dtype_to_numpy_dtype""" assert expected_numpy_dtype == convert_base_data_type_to_numpy_dtype(common_dtype) + + +@pytest.mark.parametrize( + "constant_data,expected_constructor", + [ + (10, int), + (42.0, float), + (numpy.int32(10), numpy.int32), + (numpy.array([[0, 1], [3, 4]], dtype=numpy.uint64), numpy.uint64), + (numpy.array([[0, 1], [3, 4]], dtype=numpy.float64), numpy.float64), + ], +) +def test_get_type_constructor_for_numpy_or_python_constant_data( + constant_data, expected_constructor +): + """Test function for get_type_constructor_for_numpy_or_python_constant_data""" + + assert expected_constructor == get_type_constructor_for_numpy_or_python_constant_data( + constant_data + ) From a4ddcfe88ed120504004976b5acf37b1679bcc32 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 11:56:21 +0200 Subject: [PATCH 0207/1104] fix: github actions documentation is confusing, use action for event_type --- .github/workflows/continuous-integration.yaml | 2 +- .github/workflows/docker-env.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 6d6d55ee1..947fb282a 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -106,7 +106,7 @@ jobs: path: diff-coverage.txt recreate: true - name: Trigger docker push workflow - if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.event_type == 'env-docker-preflight' }} + if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.action == 'env-docker-preflight' }} run: | curl \ -X POST \ diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index 7632a18c1..e1f0ab2e7 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -23,7 +23,7 @@ env: jobs: build_preflight_docker: - if: ${{ github.event_name != 'repository_dispatch' || github.event.event_type == 'rebuild-env-docker' }} + if: ${{ github.event_name != 'repository_dispatch' || github.event.action == 'rebuild-env-docker' }} concurrency: group: ${{ github.ref }} From 31804feaa5c24b4c14de525c9be6de61fb23e6df Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 12:14:50 +0200 Subject: [PATCH 0208/1104] chore: update release issue template and process --- .github/ISSUE_TEMPLATE/release.md | 14 +++++++------- docs/dev/howto/RELEASING.md | 8 +++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 56f7465b0..998f00f66 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -1,20 +1,20 @@ --- name: Release about: Issue template to prepare a release step by step. -title: "Release vX.Y.Z" +title: "Release vX.Y.Z (or vX.Y.Zrc?)" --- Release check-list: +- [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Zrc?` for Release Candidates) following semantic versioning: https://semver.org/ +- [ ] Update the version in pyproject.toml to `X.Y.Z` (or `vX.Y.Zrc?`) - [ ] Check the release milestone issues, cut out what can't be completed in time -- [ ] Choose the version number, e.g. `vX.Y.Z` following semantic versioning: https://semver.org/ -- [ ] Update the version in pyproject.toml to `X.Y.Z` -- [ ] Checkout the commit for release, create a signed tag with the version name `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, push it to GitHub with `git push origin refs/tags/vX.Y.Z` +- [ ] Checkout the commit for release, create a signed tag with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, push it to GitHub with `git push origin refs/tags/vX.Y.Z` - [ ] Run sanity checks inside the dev docker: `make pcc` and `make pytest && make coverage` - [ ] On the build machine with docker installed, run in your OS terminal in the project dir: `make release_docker` -- [ ] Re-tag the image with `docker tag concretefhe-release:latest ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` +- [ ] Re-tag the image with `docker tag concretefhe-release:latest ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` (or `vX.Y.Zrc?`) - [ ] `docker login ghcr.io`, input your username and GitHub Personal Access Token (PAT). If not already done add `write:packages` to your PAT -- [ ] Push the release image `docker push ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` -- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link \(`ghcr.io/zama-ai/concretefhe-release:vX.Y.Z`\) for the uploaded docker image +- [ ] Push the release image `docker push ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` (or `vX.Y.Zrc?`) +- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link \(`ghcr.io/zama-ai/concretefhe-release:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image All done! diff --git a/docs/dev/howto/RELEASING.md b/docs/dev/howto/RELEASING.md index a1b631aef..36974d20f 100644 --- a/docs/dev/howto/RELEASING.md +++ b/docs/dev/howto/RELEASING.md @@ -1,3 +1,9 @@ # Creating A Release On GitHub -Please open an issue with the release template: https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md. The check-list will guide you through what's required. +## Release Candidate cycle + +Before settling for a final release, we go through a Release Candidate (RC) cycle. The idea is that once the code base and documentations look ready for a release you create an RC Release by opening an issue with the release template here: https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md starting with version `vX.Y.Zrc0` and then with versions `vX.Y.Zrc1`, `vX.Y.Zrc2`... + +## Proper release + +Once the last RC is deemed ready, open an issue with the release template using the last RC version from which you remove the `rc?` part (i.e. `v12.67.19` if your last RC version was `v12.67.19-rc4`): https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md. From da96a63af8a107b42c2080d1095e0e630ee8256d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 13:15:55 +0200 Subject: [PATCH 0209/1104] fix: add curl to env docker image --- docker/Dockerfile.concretefhe-env | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index 86d3de0e9..39d81142f 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -2,6 +2,7 @@ FROM ghcr.io/zama-ai/zamalang-compiler RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ + curl \ python3.8 \ python3.8-tk \ python3.8-venv \ From 6d43ab81a9878cefe4350f5851c93fcda2569c97 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 9 Sep 2021 10:52:26 +0200 Subject: [PATCH 0210/1104] doc: let's document the framework refs #143 --- docs/README.md | 19 +++++- docs/dev/explanation/MLIR.md | 2 +- docs/dev/howto/DOCUMENTING.md | 6 +- docs/index.rst | 4 +- docs/user/explanation/FHE_LIMITS.md | 1 + docs/user/explanation/FUTURE_FEATURES.md | 1 + docs/user/explanation/QUANTIZATION.md | 1 + docs/user/explanation/WHAT_IS_FHE.md | 2 + docs/user/howto/COMPILE.md | 3 + docs/user/howto/DEBUG.md | 1 - .../user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md | 59 +++++++++++++++++++ docs/user/howto/FAQ.md | 13 +++- docs/user/howto/REDUCE_NEEDED_PRECISION.md | 2 + docs/user/howto/SUBMIT_ISSUE.md | 1 - docs/user/howto/SUPPORT.md | 1 - docs/user/tutorial/FIRST_TUTORIAL.md | 4 +- docs/user/tutorial/SECOND_TUTORIAL.md | 2 +- docs/user/tutorial/THIRD_TUTORIAL.md | 2 +- 18 files changed, 110 insertions(+), 14 deletions(-) delete mode 100644 docs/user/howto/DEBUG.md create mode 100644 docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md delete mode 100644 docs/user/howto/SUBMIT_ISSUE.md delete mode 100644 docs/user/howto/SUPPORT.md diff --git a/docs/README.md b/docs/README.md index dc8e892b4..3b264ca2b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,20 @@ # What is ConcreteFHE -To be done +## Introduction + +ConcreteFHE, or Concrete for short, is a python package which aims to simplify the use of so-called fully homomorphic encryption (FHE) for datascientists. FHE is a new powerful cryptographic tool, which allows e.g. servers to perform computations directly on encrypted data, without needing any kind of secret key. With FHE, privacy is at the center, and one can build services which ensure full privacy of the user and are the perfect equivalent of their unsecure counterpart. FHE is also a killer feature regarding data breaches: as anything done on the server is done over encrypted data, even if the server is compromised, there is in the end no leak of any kind of useful data. + +Concrete framework is made of several parts: +- a library, called concrete-lib, which contains the core cryptographic API's for computing with FHE +- a compiler, called concrete-compiler, which allows to turn an MLIR program into an FHE program, on the top of concrete-lib +- some frontends, which convert different langages to MLIR, to finally be compiled. + +In the first version of Concrete framework, there is a single frontend, called concrete-hnp, which is the equivalent of numpy. With our toolchain, a data scientist can convert a numpy program into an FHE program, without any a-priori knowledge on cryptography. + +## Organization of the Documentation + +Basically, we have divided our documentation into several parts: +- one about basic elements, notably description of the installation, that you are currently reading +- one dedicated to _users_ of Concrete, with tutorials, how-to's and deeper explanations +- and finally, one dedicated to _developpers_ of Concrete, who could be internal or external contributors to the framework + diff --git a/docs/dev/explanation/MLIR.md b/docs/dev/explanation/MLIR.md index 74c37dea7..ad49e6ca3 100644 --- a/docs/dev/explanation/MLIR.md +++ b/docs/dev/explanation/MLIR.md @@ -1,4 +1,4 @@ # MLIR -to be done +To be done by Ayoub, #311 diff --git a/docs/dev/howto/DOCUMENTING.md b/docs/dev/howto/DOCUMENTING.md index b208ac272..1c73f7df1 100644 --- a/docs/dev/howto/DOCUMENTING.md +++ b/docs/dev/howto/DOCUMENTING.md @@ -1,7 +1,7 @@ # Documenting -## Making docs with Sphinx +## Using Sphinx One can simply create docs with Sphinx and open them, by doing: @@ -9,7 +9,9 @@ One can simply create docs with Sphinx and open them, by doing: make docs ``` -The documentation contains both files written by hand by developpers and files automatically created by parsing the source files. +Remark that this needs to be done in docker. + +The documentation contains both files written by hand by developpers (the .md files) and files automatically created by parsing the source files. ### Opening doc diff --git a/docs/index.rst b/docs/index.rst index fcbe69845..803b85f7c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,9 +24,7 @@ Homomorphic Development Kit's documentation user/howto/COMPILE.md user/howto/REDUCE_NEEDED_PRECISION.md - user/howto/DEBUG.md - user/howto/SUBMIT_ISSUE.md - user/howto/SUPPORT.md + user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md user/howto/FAQ.md .. toctree:: diff --git a/docs/user/explanation/FHE_LIMITS.md b/docs/user/explanation/FHE_LIMITS.md index df9defe8c..4c69ca8a6 100644 --- a/docs/user/explanation/FHE_LIMITS.md +++ b/docs/user/explanation/FHE_LIMITS.md @@ -1,2 +1,3 @@ # FHE Limits +Benoit to do: write it \ No newline at end of file diff --git a/docs/user/explanation/FUTURE_FEATURES.md b/docs/user/explanation/FUTURE_FEATURES.md index 2ee513008..1e2537417 100644 --- a/docs/user/explanation/FUTURE_FEATURES.md +++ b/docs/user/explanation/FUTURE_FEATURES.md @@ -1,2 +1,3 @@ # Future Features +Alex to do: #321 \ No newline at end of file diff --git a/docs/user/explanation/QUANTIZATION.md b/docs/user/explanation/QUANTIZATION.md index 7ab168d56..e007a4481 100644 --- a/docs/user/explanation/QUANTIZATION.md +++ b/docs/user/explanation/QUANTIZATION.md @@ -1,2 +1,3 @@ # Quantization +Arthur to do: #319 \ No newline at end of file diff --git a/docs/user/explanation/WHAT_IS_FHE.md b/docs/user/explanation/WHAT_IS_FHE.md index ce4af9382..233285aab 100644 --- a/docs/user/explanation/WHAT_IS_FHE.md +++ b/docs/user/explanation/WHAT_IS_FHE.md @@ -1 +1,3 @@ # What is FHE? + +Benoit to do: write it diff --git a/docs/user/howto/COMPILE.md b/docs/user/howto/COMPILE.md index 6d6a98c48..77627afdc 100644 --- a/docs/user/howto/COMPILE.md +++ b/docs/user/howto/COMPILE.md @@ -1 +1,4 @@ # Compiling + +Umut or Arthur, who wants to do this part? + diff --git a/docs/user/howto/DEBUG.md b/docs/user/howto/DEBUG.md deleted file mode 100644 index 1e584c954..000000000 --- a/docs/user/howto/DEBUG.md +++ /dev/null @@ -1 +0,0 @@ -# Debugging diff --git a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md new file mode 100644 index 000000000..7660f0f73 --- /dev/null +++ b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -0,0 +1,59 @@ +# Debugging / Support / Submitting Issues + +First, let's not forget that this version of Concrete framework is a beta product, meaning that it is not completely polished, contains several bugs (would they be known or unknown at this time). Also, let's not forget that FHE is an highly hot topic, and notably, that it cannot be considered as a solved problem. + +Anyway, let's list some ways to debug your problems here. If nothing seems conclusive, you can still report the issue, as explained in a later section of this page. + +## Is it a bug by the framework or by the user? + +If ever your numpy program fails, it may be because: +- of bugs due to Concrete framework +- of bugs due to the user, notably who would not use the framework as expected or not consider the limits of the framework. + +For the latter kind of bugs, we encourage the user to have a look to: +- the error message she gets +- the documentation of the product +- the known limits of the product (such as the reduced set of supported operations at this time, or the limited precision of the computations). + +Once the user has tried to see if the bug was not her own, it is time to go further. + +## Having a reproducible bug + +Once you're sure it is a bug, it would be nice to try to +- make it highly reproducible: e.g., by reducing as much the randomness as possible; e.g., if you can find an input which fails, there is no reason to let the input random +- reduce it to the smallest possible bug: it is easier to investigate bugs which are small, so when you have an issue, please try to reduce to a smaller issue, notably with less lines of code, smaller parameters, less complex function to compile, faster scripts etc + +## Asking the community + +We have created a Slack channel (TODO: LINK TO BE ADDED), such that you can directly ask the developpers and community about your issue. + +Hopefully, it is just a misunderstanding or a small mistake on your side, that one can help you fix easily. And, the good point with your feedback is that, once we have heard the problem or misunderstanding, we can make the documentation even clearer (such as, increasing the FAQ). + +## Having a look to the compilation artifacts + +When things are more complicated, or if you want to have a look by yourself, you may want to have a look to the compilation reports, which are called artifacts. This is as simple as described in [TODO: add the link to the tutorial about having artifacts]. + +This function will create a directory, containing notably: +[TODO: Umut to fix / complete the following information] +- bounds.txt: a file describing the expected ranges of data in the different steps of the computation +- cryptographic_parameters.txt: a file describing the different keys +- ir_nodes.txt: a file describing the different nodes in the intermediate representation (IR) +- optimizations_applied.txt: a file describing the different optimizations which were applied +- target_nodes.txt: a file describing the different nodes in the VM graph + +Attaching the artifact with your issue or Slack message may help people to have a look at the core of the problem. +The more precise your bug, the more likely we can reproduce and fix + +[TODO: Umut, is it still needed or do we already have some of those information in artifacts?] +In order to simplify our work and let us reproduce your bug easily, any information is useful. Notably, in addition to the python script, some information like +- the OS version +- the python version +- the python packages you use +- the reproducibility rate you see on your side +- any insight you might have on the bug +- any workaround you have been able to find +may be useful to us. Don't remember, Concrete is a project where we are open to contribution, more information at Contributing (TODO: add a link). + +## Submitting an issue + +In case you have a bug, which is reproducible, that you have reduced to a small piece of code, we have our issue tracker (TODO: LINK TO BE ADDED). Remember that a well-described short issue is an issue which is more likely to be studied and fixed. The more issues we receive, the better the product will be. diff --git a/docs/user/howto/FAQ.md b/docs/user/howto/FAQ.md index 9a2b02bd1..7b976a960 100644 --- a/docs/user/howto/FAQ.md +++ b/docs/user/howto/FAQ.md @@ -1,3 +1,14 @@ # FAQ -to be done +## What is Concrete FHE? + +See [here](../../README.md) + +## Is it an open source project? + +## Can I use it freely? + +## Can I contribute? + +## What are the future features of Concrete? + diff --git a/docs/user/howto/REDUCE_NEEDED_PRECISION.md b/docs/user/howto/REDUCE_NEEDED_PRECISION.md index 6877dc089..02fd87227 100644 --- a/docs/user/howto/REDUCE_NEEDED_PRECISION.md +++ b/docs/user/howto/REDUCE_NEEDED_PRECISION.md @@ -1 +1,3 @@ # Having a Function Which Requires Less Precision + +Arthur to do: #319 \ No newline at end of file diff --git a/docs/user/howto/SUBMIT_ISSUE.md b/docs/user/howto/SUBMIT_ISSUE.md deleted file mode 100644 index 3e72eafda..000000000 --- a/docs/user/howto/SUBMIT_ISSUE.md +++ /dev/null @@ -1 +0,0 @@ -# Submitting Issues diff --git a/docs/user/howto/SUPPORT.md b/docs/user/howto/SUPPORT.md deleted file mode 100644 index 85d1c2942..000000000 --- a/docs/user/howto/SUPPORT.md +++ /dev/null @@ -1 +0,0 @@ -# Support diff --git a/docs/user/tutorial/FIRST_TUTORIAL.md b/docs/user/tutorial/FIRST_TUTORIAL.md index 6c1c7b9bc..4e747fe30 100644 --- a/docs/user/tutorial/FIRST_TUTORIAL.md +++ b/docs/user/tutorial/FIRST_TUTORIAL.md @@ -1,3 +1,5 @@ # First Tutorial -To be continued +Umut to do: #312 + + diff --git a/docs/user/tutorial/SECOND_TUTORIAL.md b/docs/user/tutorial/SECOND_TUTORIAL.md index 9167da9e6..26642fb74 100644 --- a/docs/user/tutorial/SECOND_TUTORIAL.md +++ b/docs/user/tutorial/SECOND_TUTORIAL.md @@ -1,3 +1,3 @@ # Second Tutorial -To be continued +Umut to do: #313 diff --git a/docs/user/tutorial/THIRD_TUTORIAL.md b/docs/user/tutorial/THIRD_TUTORIAL.md index 44fd7293d..661b4e60f 100644 --- a/docs/user/tutorial/THIRD_TUTORIAL.md +++ b/docs/user/tutorial/THIRD_TUTORIAL.md @@ -1,3 +1,3 @@ # Third Tutorial -To be continued +Umut to do: #314 From 390429a7831a0c226f8945dc993ac1d962a977ce Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 9 Sep 2021 13:12:40 +0200 Subject: [PATCH 0211/1104] doc: let's document the framework refs #143 --- docs/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 3b264ca2b..d2a5916ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,10 +11,22 @@ Concrete framework is made of several parts: In the first version of Concrete framework, there is a single frontend, called concrete-hnp, which is the equivalent of numpy. With our toolchain, a data scientist can convert a numpy program into an FHE program, without any a-priori knowledge on cryptography. -## Organization of the Documentation +## Organization of the documentation Basically, we have divided our documentation into several parts: - one about basic elements, notably description of the installation, that you are currently reading - one dedicated to _users_ of Concrete, with tutorials, how-to's and deeper explanations - and finally, one dedicated to _developpers_ of Concrete, who could be internal or external contributors to the framework +## A work in progress + +Concrete is a work in progress, and is currently limited to a certain number of operators and features. In the future, there will be improvements as described in this [section](user/explanation/FUTURE_FEATURES.md). + +The main _current_ limits are: +- Concrete is only supporting unsigned integers +- Concrete needs the integer to be less than 7 bits (included) +- Concrete is mostly restricted to scalars (by opposition to tensors). The only exception is the `dot` operator, which can dot a tensor of encrypted values with a tensor of constant values + +The first two limits can be taken care of with the use of quantization, as explained a bit further in [this](user/explanation/QUANTIZATION.md) and [this](user/howto/REDUCE_NEEDED_PRECISION.md) parts of the documentation. + +The scalar limitation is mainly an engineering issue, and will be fixed in the next release. Today, one needs to split all the tensors into small scalars, which is inconvenient and will be no more needed very soon. From e747dd819af76bec4b20a8bf2785444336dcf53a Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 9 Sep 2021 13:31:01 +0200 Subject: [PATCH 0212/1104] doc: let's document the framework refs #143 --- docs/README.md | 6 +++--- docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/README.md b/docs/README.md index d2a5916ff..712f8ca6b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,14 +2,14 @@ ## Introduction -ConcreteFHE, or Concrete for short, is a python package which aims to simplify the use of so-called fully homomorphic encryption (FHE) for datascientists. FHE is a new powerful cryptographic tool, which allows e.g. servers to perform computations directly on encrypted data, without needing any kind of secret key. With FHE, privacy is at the center, and one can build services which ensure full privacy of the user and are the perfect equivalent of their unsecure counterpart. FHE is also a killer feature regarding data breaches: as anything done on the server is done over encrypted data, even if the server is compromised, there is in the end no leak of any kind of useful data. +ConcreteFHE, or Concrete for short, is an open-source framework which aims to simplify the use of so-called fully homomorphic encryption (FHE) for data scientists. FHE is a new powerful cryptographic tool, which allows e.g. servers to perform computations directly on encrypted data, without needing to decrypt first. With FHE, privacy is at the center, and one can build services which ensure full privacy of the user and are the perfect equivalent of their unsecure counterpart. FHE is also a killer feature regarding data breaches: as anything done on the server is done over encrypted data, even if the server is compromised, there is in the end no leak of any kind of useful data. -Concrete framework is made of several parts: +The Concrete framework is made of several parts: - a library, called concrete-lib, which contains the core cryptographic API's for computing with FHE - a compiler, called concrete-compiler, which allows to turn an MLIR program into an FHE program, on the top of concrete-lib - some frontends, which convert different langages to MLIR, to finally be compiled. -In the first version of Concrete framework, there is a single frontend, called concrete-hnp, which is the equivalent of numpy. With our toolchain, a data scientist can convert a numpy program into an FHE program, without any a-priori knowledge on cryptography. +In the first version of Concrete framework, there is a single frontend, called homomorphic numpy (or hnp), which is the equivalent of numpy. With our toolchain, a data scientist can convert a numpy program into an FHE program, without any a-priori knowledge on cryptography. ## Organization of the documentation diff --git a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md index 7660f0f73..b45a13428 100644 --- a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md +++ b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -1,6 +1,6 @@ # Debugging / Support / Submitting Issues -First, let's not forget that this version of Concrete framework is a beta product, meaning that it is not completely polished, contains several bugs (would they be known or unknown at this time). Also, let's not forget that FHE is an highly hot topic, and notably, that it cannot be considered as a solved problem. +First, let's not forget that this version of Concrete framework is a beta product, meaning that it is not completely polished, contains several bugs (would they be known or unknown at this time). Also, let's not forget that FHE is a highly hot topic, and notably, that it cannot be considered as a solved problem. Anyway, let's list some ways to debug your problems here. If nothing seems conclusive, you can still report the issue, as explained in a later section of this page. @@ -8,9 +8,9 @@ Anyway, let's list some ways to debug your problems here. If nothing seems concl If ever your numpy program fails, it may be because: - of bugs due to Concrete framework -- of bugs due to the user, notably who would not use the framework as expected or not consider the limits of the framework. +- of bugs due to the user, notably who would have a bug without even considering FHE (does the function you want to compile run well with numpy?), or who would not use the framework as expected or not consider the limits of the framework. -For the latter kind of bugs, we encourage the user to have a look to: +For the latter kind of bugs, we encourage the user to have a look at: - the error message she gets - the documentation of the product - the known limits of the product (such as the reduced set of supported operations at this time, or the limited precision of the computations). @@ -19,15 +19,15 @@ Once the user has tried to see if the bug was not her own, it is time to go furt ## Having a reproducible bug -Once you're sure it is a bug, it would be nice to try to +Once you're sure it is a bug, it would be nice to try to: - make it highly reproducible: e.g., by reducing as much the randomness as possible; e.g., if you can find an input which fails, there is no reason to let the input random -- reduce it to the smallest possible bug: it is easier to investigate bugs which are small, so when you have an issue, please try to reduce to a smaller issue, notably with less lines of code, smaller parameters, less complex function to compile, faster scripts etc +- reduce it to the smallest possible bug: it is easier to investigate bugs which are small, so when you have an issue, please try to reduce to a smaller issue, notably with less lines of code, smaller parameters, less complex function to compile, faster scripts etc. ## Asking the community We have created a Slack channel (TODO: LINK TO BE ADDED), such that you can directly ask the developpers and community about your issue. -Hopefully, it is just a misunderstanding or a small mistake on your side, that one can help you fix easily. And, the good point with your feedback is that, once we have heard the problem or misunderstanding, we can make the documentation even clearer (such as, increasing the FAQ). +Hopefully, it is just a misunderstanding or a small mistake on your side, that one can help you fix easily. And, the good point with your feedback is that, once we have heard the problem or misunderstanding, we can make the documentation even clearer (such as, completing the FAQ). ## Having a look to the compilation artifacts @@ -45,7 +45,7 @@ Attaching the artifact with your issue or Slack message may help people to have The more precise your bug, the more likely we can reproduce and fix [TODO: Umut, is it still needed or do we already have some of those information in artifacts?] -In order to simplify our work and let us reproduce your bug easily, any information is useful. Notably, in addition to the python script, some information like +In order to simplify our work and let us reproduce your bug easily, any information is useful. Notably, in addition to the python script, some information like: - the OS version - the python version - the python packages you use From 71f97e9e445c84d372d136d1f9d8ea954c835056 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 13:48:58 +0200 Subject: [PATCH 0213/1104] fix: missed event_type now converted to action --- .github/workflows/docker-env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index e1f0ab2e7..f597bace2 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -77,7 +77,7 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} push-docker-image: - if: ${{ github.event_name == 'repository_dispatch' && github.event.event_type == 'publish-env-docker'}} + if: ${{ github.event_name == 'repository_dispatch' && github.event.action == 'publish-env-docker'}} concurrency: group: ${{ github.ref }} From 0c4178a6fd49ebb82c55364b88cb5e59709cd48f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 14:06:04 +0200 Subject: [PATCH 0214/1104] chore: fix typo in release issue template, fix rc numbering --- .github/ISSUE_TEMPLATE/release.md | 4 ++-- docs/dev/howto/RELEASING.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 998f00f66..dc7f0c6c3 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -7,9 +7,9 @@ title: "Release vX.Y.Z (or vX.Y.Zrc?)" Release check-list: - [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Zrc?` for Release Candidates) following semantic versioning: https://semver.org/ -- [ ] Update the version in pyproject.toml to `X.Y.Z` (or `vX.Y.Zrc?`) +- [ ] Update the version in pyproject.toml to `X.Y.Z` (or `X.Y.Zrc?`) - [ ] Check the release milestone issues, cut out what can't be completed in time -- [ ] Checkout the commit for release, create a signed tag with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, push it to GitHub with `git push origin refs/tags/vX.Y.Z` +- [ ] Checkout the commit for release, create a signed tag with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Zrc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Zrc?`) - [ ] Run sanity checks inside the dev docker: `make pcc` and `make pytest && make coverage` - [ ] On the build machine with docker installed, run in your OS terminal in the project dir: `make release_docker` - [ ] Re-tag the image with `docker tag concretefhe-release:latest ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` (or `vX.Y.Zrc?`) diff --git a/docs/dev/howto/RELEASING.md b/docs/dev/howto/RELEASING.md index 36974d20f..086c3733f 100644 --- a/docs/dev/howto/RELEASING.md +++ b/docs/dev/howto/RELEASING.md @@ -2,7 +2,7 @@ ## Release Candidate cycle -Before settling for a final release, we go through a Release Candidate (RC) cycle. The idea is that once the code base and documentations look ready for a release you create an RC Release by opening an issue with the release template here: https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md starting with version `vX.Y.Zrc0` and then with versions `vX.Y.Zrc1`, `vX.Y.Zrc2`... +Before settling for a final release, we go through a Release Candidate (RC) cycle. The idea is that once the code base and documentations look ready for a release you create an RC Release by opening an issue with the release template here: https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md starting with version `vX.Y.Zrc1` and then with versions `vX.Y.Zrc2`, `vX.Y.Zrc3`... ## Proper release From 06be053a35fe9cbe86249786f2419173dc5ba276 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 14:56:43 +0200 Subject: [PATCH 0215/1104] fix: bad rename for docker release image requirements --- docker/Dockerfile.release | 8 ++++---- docker/Dockerfile.release.dockerignore | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index fc072d8e7..5b9577060 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -1,4 +1,4 @@ -FROM ghcr.io/zama-ai/zamalang-compiler as builder +FROM ghcr.io/zama-ai/zamalang-compiler:bb0126a86a36b9062760c97b7e2f9d7008549899 as builder RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ @@ -14,12 +14,12 @@ COPY pyproject.toml ./pyproject.toml RUN poetry build --format wheel -FROM ghcr.io/zama-ai/zamalang-compiler +FROM ghcr.io/zama-ai/zamalang-compiler:bb0126a86a36b9062760c97b7e2f9d7008549899 RUN mkdir /pkg && mkdir /app WORKDIR /pkg COPY --from=builder /build/dist/*.whl . -COPY docker/datascience_requirements.txt . +COPY docker/release_requirements.txt . COPY torch_requirements.txt . RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ @@ -35,7 +35,7 @@ RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ python3 -m pip install --no-cache-dir ./*.whl && \ python3 -m pip install --no-cache-dir -r torch_requirements.txt \ -f https://download.pytorch.org/whl/torch_stable.html && \ - python3 -m pip install --no-cache-dir -r datascience_requirements.txt + python3 -m pip install --no-cache-dir -r release_requirements.txt WORKDIR /app RUN printf "#!/bin/bash\npython3 -m jupyter notebook --ip=0.0.0.0 --allow-root --no-browser\n" \ diff --git a/docker/Dockerfile.release.dockerignore b/docker/Dockerfile.release.dockerignore index 06fbab269..deffd550c 100644 --- a/docker/Dockerfile.release.dockerignore +++ b/docker/Dockerfile.release.dockerignore @@ -4,7 +4,7 @@ # Not our sources !concrete !pyproject.toml -!docker/datascience_requirements.txt +!docker/release_requirements.txt !torch_requirements.txt # But still ignore pycache From 42777d8888fa13fb03e3086baa024fd628951ee2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 15:08:01 +0200 Subject: [PATCH 0216/1104] docs: Re-organize installation docs - add user docs to use concretefhe with docker --- README.md | 35 +++++++++---- docs/{install => dev/howto}/DOCKER.md | 0 .../howto/PROJECT_SETUP.md} | 2 +- docs/index.rst | 6 +-- docs/user/howto/INSTALLING.md | 50 +++++++++++++++++++ 5 files changed, 79 insertions(+), 14 deletions(-) rename docs/{install => dev/howto}/DOCKER.md (100%) rename docs/{install/INSTALLING.md => dev/howto/PROJECT_SETUP.md} (99%) create mode 100644 docs/user/howto/INSTALLING.md diff --git a/README.md b/README.md index 1134bb33f..5beaf9126 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,40 @@ Concrete Framework Python API - collection of tools to FHE all the things -## Installing + -Installation steps are described in [INSTALLING.md](docs/install/INSTALLING.md). +- [concretefhe](#concretefhe) + - [For end users](#for-end-users) + - [Using the project](#using-the-project) + - [For developers](#for-developers) + - [Project setup](#project-setup) + - [Documenting](#documenting) + - [Developing](#developing) + - [Contributing](#contributing) -## Using Docker + -Information about how to use Docker are available in [DOCKER.md](docs/install/DOCKER.md). +## For end users -## Documenting +### Using the project -Some information about how to build the documentation of `concretefhe` are available in [DOCUMENTING.md](docs/dev/DOCUMENTING.md). Notably, our documentation is pushed to [https://hdk.zama.ai](https://hdk.zama.ai). +To use the project you can check [INSTALLING.md](docs/user/howto/INSTALLING.md) -## Developping +## For developers -Some information about our terminology and the infrastructure of `concretefhe` are available in [TERMINOLOGY_AND_STRUCTURE.md](docs/dev/TERMINOLOGY_AND_STRUCTURE.md). An in-depth look at what is done in `concretefhe` is available in [COMPILATION.md](docs/dev/COMPILATION.md). +### Project setup -## Contributing +Installation steps are described in [PROJECT_SETUP.md](docs/dev/howto/PROJECT_SETUP.md). +Information about how to use Docker for development are available in [DOCKER.md](docs/dev/howto/DOCKER.md). -Information about how to contribute are available in [CONTRIBUTING.md](docs/dev/CONTRIBUTING.md). +### Documenting +Some information about how to build the documentation of `concretefhe` are available in [DOCUMENTING.md](docs/dev/howto/DOCUMENTING.md). Notably, our documentation is pushed to [https://hdk.zama.ai](https://hdk.zama.ai). +### Developing +Some information about our terminology and the infrastructure of `concretefhe` are available in [TERMINOLOGY_AND_STRUCTURE.md](docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md). An in-depth look at what is done in `concretefhe` is available in [COMPILATION.md](docs/dev/explanation/COMPILATION.md). + +### Contributing + +Information about how to contribute are available in [CONTRIBUTING.md](docs/dev/howto/CONTRIBUTING.md). diff --git a/docs/install/DOCKER.md b/docs/dev/howto/DOCKER.md similarity index 100% rename from docs/install/DOCKER.md rename to docs/dev/howto/DOCKER.md diff --git a/docs/install/INSTALLING.md b/docs/dev/howto/PROJECT_SETUP.md similarity index 99% rename from docs/install/INSTALLING.md rename to docs/dev/howto/PROJECT_SETUP.md index a0408c683..f411074aa 100644 --- a/docs/install/INSTALLING.md +++ b/docs/dev/howto/PROJECT_SETUP.md @@ -1,5 +1,5 @@ -# Installing +# Project Setup ## Installing Python v3.8 diff --git a/docs/index.rst b/docs/index.rst index 803b85f7c..97700d36b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,8 +7,6 @@ Homomorphic Development Kit's documentation :caption: Basics README.md - install/INSTALLING.md - install/DOCKER.md .. toctree:: :maxdepth: 2 @@ -22,6 +20,7 @@ Homomorphic Development Kit's documentation :maxdepth: 2 :caption: How to + user/howto/INSTALLING.md user/howto/COMPILE.md user/howto/REDUCE_NEEDED_PRECISION.md user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -46,6 +45,8 @@ Homomorphic Development Kit's documentation :maxdepth: 2 :caption: Developper - How To + dev/howto/PROJECT_SETUP.md + dev/howto/DOCKER.md dev/howto/DOCUMENTING.md Releasing on GitHub dev/howto/CONTRIBUTING.md @@ -58,4 +59,3 @@ Homomorphic Development Kit's documentation dev/explanation/TERMINOLOGY_AND_STRUCTURE.md dev/explanation/FLOAT-FUSING.md dev/explanation/MLIR.md - \ No newline at end of file diff --git a/docs/user/howto/INSTALLING.md b/docs/user/howto/INSTALLING.md new file mode 100644 index 000000000..126e2b5e1 --- /dev/null +++ b/docs/user/howto/INSTALLING.md @@ -0,0 +1,50 @@ +# Installing + +## Docker image + +Currently the project is only available as a docker image. To get the image you need to login to ghcr.io with docker. + +```shell +docker login ghcr.io +``` + +This command will ask for a username and a password. For username, just enter your GitHub username. For password, you should create a personal access token from [here](https://github.com/settings/tokens) selecting `read:packages` permission. Just paste the generated access token as your password, and you are good to go. + +You can then either pull the latest docker image or a specific version: + +```shell +docker pull ghcr.io/zama-ai/concretefhe-internal:latest +# or +docker pull ghcr.io/zama-ai/concretefhe-internal:v0.1.0 +``` + +You can then use this image with the following command: + +```shell +# Without local volume: +docker run --rm -it -p 8888:8888 ghcr.io/zama-ai/concretefhe-internal:v0.1.0 + +# With local volume to save notebooks on host: +docker run --rm -it -p 8888:8888 -v /host/path:/data ghcr.io/zama-ai/concretefhe-internal:v0.1.0 +``` + +This will launch a concretefhe enabled jupyter server in the docker, that you can access from your browser. + +Alternatively you can just open a shell in the docker: + +```shell +docker run --rm -it ghcr.io/zama-ai/concretefhe-internal:v0.1.0 /bin/bash + +root@e2d6c00e2f3d:/data# python3 +Python 3.8.10 (default, Jun 2 2021, 10:49:15) +[GCC 9.4.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import concrete.numpy as hnp +>>> dir(hnp) +['ClearScalar', 'ClearTensor', 'CompilationArtifacts', 'CompilationConfiguration', 'EncryptedScalar', 'EncryptedTensor', 'Float', 'Float32', 'Float64', 'Integer', 'LookupTable', 'ScalarValue', 'SignedInteger', 'TensorValue', 'UnsignedInteger', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'compile', 'compile_numpy_function', 'compile_numpy_function_into_op_graph', 'draw_graph', 'get_printable_graph', 'np_dtypes_helpers', 'trace_numpy_function', 'tracing'] +>>> +``` + + + + From 6203822a9bd6f3a97affc885eec0fe4cb738865a Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 9 Sep 2021 14:44:17 +0200 Subject: [PATCH 0217/1104] doc: let's document the framework refs #143 --- docs/README.md | 6 ++-- docs/_static/css/zama.css | 4 ++- docs/conf.py | 2 +- docs/index.rst | 7 ++-- .../explanation/FHE_AND_FRAMEWORK_LIMITS.md | 35 +++++++++++++++++++ docs/user/explanation/FHE_LIMITS.md | 3 -- docs/user/explanation/FUTURE_FEATURES.md | 2 +- docs/user/explanation/QUANTIZATION.md | 2 +- docs/user/explanation/WHAT_IS_FHE.md | 11 +++++- docs/user/howto/COMPILE.md | 4 --- docs/user/howto/COMPILING_AND_EXECUTING.md | 10 ++++++ .../user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md | 2 +- docs/user/howto/REDUCE_NEEDED_PRECISION.md | 2 +- 13 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md delete mode 100644 docs/user/explanation/FHE_LIMITS.md delete mode 100644 docs/user/howto/COMPILE.md create mode 100644 docs/user/howto/COMPILING_AND_EXECUTING.md diff --git a/docs/README.md b/docs/README.md index 712f8ca6b..da76cc35f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,13 +20,13 @@ Basically, we have divided our documentation into several parts: ## A work in progress -Concrete is a work in progress, and is currently limited to a certain number of operators and features. In the future, there will be improvements as described in this [section](user/explanation/FUTURE_FEATURES.md). +Concrete is a work in progress, and is currently limited to a certain number of operators and features. In the future, there will be improvements as described in this [section](user/explanation/FUTURE_FEATURES.md). The main _current_ limits are: - Concrete is only supporting unsigned integers - Concrete needs the integer to be less than 7 bits (included) - Concrete is mostly restricted to scalars (by opposition to tensors). The only exception is the `dot` operator, which can dot a tensor of encrypted values with a tensor of constant values -The first two limits can be taken care of with the use of quantization, as explained a bit further in [this](user/explanation/QUANTIZATION.md) and [this](user/howto/REDUCE_NEEDED_PRECISION.md) parts of the documentation. +The first two limits can be taken care of with the use of quantization, as explained a bit further in [this](user/explanation/QUANTIZATION.md) and [this](user/howto/REDUCE_NEEDED_PRECISION.md) parts of the documentation. -The scalar limitation is mainly an engineering issue, and will be fixed in the next release. Today, one needs to split all the tensors into small scalars, which is inconvenient and will be no more needed very soon. +The scalar limitation is mainly an engineering issue, and will be fixed in the next release. Today, one needs to split all the tensors into small scalars, which is inconvenient and will be no more needed very soon. diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index 03162bef8..8ae310ebe 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -26,7 +26,9 @@ top: 0; left: 50%; padding: 0; - margin: 0 0 0 -100px !important; + + /* Adjust this to change the adjustment of the Zama's logo, on the top left */ + margin: 0 0 0 -80px !important; } .rst-content code.literal, .rst-content tt.literal { diff --git a/docs/conf.py b/docs/conf.py index fafee81ea..803fd3289 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,7 @@ # -- Project information ----------------------------------------------------- -project = "Homomorphic Development Kit" +project = "Concrete Framework" copyright = "2021, Zama" author = "Zama" diff --git a/docs/index.rst b/docs/index.rst index 97700d36b..db6e5e7ea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -Homomorphic Development Kit's documentation +Concrete Framework's documentation =========================================== @@ -7,6 +7,7 @@ Homomorphic Development Kit's documentation :caption: Basics README.md + Installing .. toctree:: :maxdepth: 2 @@ -21,7 +22,7 @@ Homomorphic Development Kit's documentation :caption: How to user/howto/INSTALLING.md - user/howto/COMPILE.md + user/howto/COMPILING_AND_EXECUTING.md user/howto/REDUCE_NEEDED_PRECISION.md user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md user/howto/FAQ.md @@ -31,7 +32,7 @@ Homomorphic Development Kit's documentation :caption: Explanation user/explanation/WHAT_IS_FHE.md - user/explanation/FHE_LIMITS.md + user/explanation/FHE_AND_FRAMEWORK_LIMITS.md user/explanation/QUANTIZATION.md user/explanation/FUTURE_FEATURES.md diff --git a/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md b/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md new file mode 100644 index 000000000..83b7f580e --- /dev/null +++ b/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md @@ -0,0 +1,35 @@ +# FHE and Concrete Framework Limits + +## FHE Limits + +FHE used to be an impossible thing to imagine, twenty years ago. Then, with advances due to [Craig Gentry](https://crypto.stanford.edu/craig/), this became a dream come true. And, even more recently, with several generations of new scheme, FHE became practical. + +### Speed + +However, one still has to consider that FHE is slow, as compared to the vanilla implementations. With the different HW pluggins that can be added to Concrete, an important speed factor can be achieved. + +### Multiplying by constants + +In the scheme used in the Concrete Framework, namely [TFHE](https://tfhe.github.io/tfhe/), multiplications by constants is only defined for integer constants. Notably, one can't multiply by floats. As float multiplication is very usual in the data science (think of weights of dense layers, for example), this could be a problem, but quantization is at our rescue. See [this](QUANTIZATION.md) section for more details. + +### Achieving computations of not-linear functions + +For most FHE scheme but TFHE, the application of a non-linear function is complicated and slow, if not impossible. Typically, this is a blocker, since activation functions _are_ non-linear. However, in the Concrete Framework, we use an operation called _programmable bootstrapping_ (described in this [white paper](https://whitepaper.zama.ai)), which allows to apply any table lookup: by quantizing the non-linear function, any function can thus be replaced. + +## Concrete Framework Limits + +Since this is an early version of the product, not everything is done, to say the least. What we wanted to tackle first was the cryptographic complexities. This is why we concentrated on the cryptographic part, and let some engineering problems for later. + +### Limited to scalars + +Today, the Concrete Framework is mostly limited to scalars. Notably, in our numpy frontend, we can not use [tensors](https://numpy.org/doc/stable/user/theory.broadcasting.html?highlight=vector). As explained in [this section](FUTURE_FEATURES.md), this limit will be removed in the next version. + +### Currently executing locally + +As of today, the execution of the FHE program is done locally. Notably, in the current version, there is no client (on which we encrypt the private data, or decrypt the returned result) or server (on which the computation is done completely over encrypted data), but a single host. As explained in [this section](FUTURE_FEATURES.md), this limit will be removed in the next version, such that the Concrete Framework can be used in production. + +### Currently slow + +As we explained, we wanted to focus first on cryptographic challenges. Performance has been postponed, and will be tackled in the next release. + + diff --git a/docs/user/explanation/FHE_LIMITS.md b/docs/user/explanation/FHE_LIMITS.md deleted file mode 100644 index 4c69ca8a6..000000000 --- a/docs/user/explanation/FHE_LIMITS.md +++ /dev/null @@ -1,3 +0,0 @@ -# FHE Limits - -Benoit to do: write it \ No newline at end of file diff --git a/docs/user/explanation/FUTURE_FEATURES.md b/docs/user/explanation/FUTURE_FEATURES.md index 1e2537417..a66f11b3f 100644 --- a/docs/user/explanation/FUTURE_FEATURES.md +++ b/docs/user/explanation/FUTURE_FEATURES.md @@ -1,3 +1,3 @@ # Future Features -Alex to do: #321 \ No newline at end of file +Alex to do: #321 diff --git a/docs/user/explanation/QUANTIZATION.md b/docs/user/explanation/QUANTIZATION.md index e007a4481..d93d53925 100644 --- a/docs/user/explanation/QUANTIZATION.md +++ b/docs/user/explanation/QUANTIZATION.md @@ -1,3 +1,3 @@ # Quantization -Arthur to do: #319 \ No newline at end of file +Arthur to do: #319 diff --git a/docs/user/explanation/WHAT_IS_FHE.md b/docs/user/explanation/WHAT_IS_FHE.md index 233285aab..9acdb3bd2 100644 --- a/docs/user/explanation/WHAT_IS_FHE.md +++ b/docs/user/explanation/WHAT_IS_FHE.md @@ -1,3 +1,12 @@ # What is FHE? -Benoit to do: write it +Fully Homomorphic Encryption (FHE for short) is a technology that enables computing on encrypted data directly, without having to decrypt it. +Users would encrypt their data using their own secret key, then send it to your servers for processing. Your servers would process the encrypted data blindly, producing a result which itself is encrypted, and that only the user can decrypt with their secret key. + +From the user's perspective, nothing changes (but the fact that her data is never in clear on the server), they are still sending data to your service and getting a response. But you now no longer need to worry about securing your user data, as it is now encrypted both in transit and during processing, i.e., it is encrypted end-to-end. + +You can learn more about FHE using the following links: +- [quick overview](https://6min.zama.ai/) +- [monthly technical FHE.org meetup](https://www.meetup.com/fhe-org/) +- [videos and resources](http://fhe.org/) + diff --git a/docs/user/howto/COMPILE.md b/docs/user/howto/COMPILE.md deleted file mode 100644 index 77627afdc..000000000 --- a/docs/user/howto/COMPILE.md +++ /dev/null @@ -1,4 +0,0 @@ -# Compiling - -Umut or Arthur, who wants to do this part? - diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md new file mode 100644 index 000000000..26b7a06dd --- /dev/null +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -0,0 +1,10 @@ +# Compiling and Executing + +Umut or Arthur, who wants to do this part? + +## Compiling + +## Executing + + + diff --git a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md index b45a13428..d8f037e97 100644 --- a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md +++ b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -56,4 +56,4 @@ may be useful to us. Don't remember, Concrete is a project where we are open to ## Submitting an issue -In case you have a bug, which is reproducible, that you have reduced to a small piece of code, we have our issue tracker (TODO: LINK TO BE ADDED). Remember that a well-described short issue is an issue which is more likely to be studied and fixed. The more issues we receive, the better the product will be. +In case you have a bug, which is reproducible, that you have reduced to a small piece of code, we have our issue tracker (TODO: LINK TO BE ADDED). Remember that a well-described short issue is an issue which is more likely to be studied and fixed. The more issues we receive, the better the product will be. diff --git a/docs/user/howto/REDUCE_NEEDED_PRECISION.md b/docs/user/howto/REDUCE_NEEDED_PRECISION.md index 02fd87227..367a4360b 100644 --- a/docs/user/howto/REDUCE_NEEDED_PRECISION.md +++ b/docs/user/howto/REDUCE_NEEDED_PRECISION.md @@ -1,3 +1,3 @@ # Having a Function Which Requires Less Precision -Arthur to do: #319 \ No newline at end of file +Arthur to do: #319 From f9854b5b790dd1e8c13850e189d8104ce7b6bfcd Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 10 Sep 2021 11:53:06 +0200 Subject: [PATCH 0218/1104] doc: update pyproject.toml to reflect what does the package and who are the authors closes #334. refs #318 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cc9d9c71b..f368f0507 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [tool.poetry] name = "concretefhe" version = "0.1.0" -description = "Concrete Framework Python API" -authors = ["A. Meyre", "U. Sahin", "A. Benaissa", "B. Chevallier", "Zama Team"] +description = "Concrete Framework" +authors = ["Zama "] packages = [ { include = "concrete" }, ] From 17822a5417d5749e1254c8f51581761ea6583983 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 10 Sep 2021 11:53:43 +0200 Subject: [PATCH 0219/1104] release: update the version refs #318 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f368f0507..ecc14b968 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.1.0" +version = "0.1.0rc1" description = "Concrete Framework" authors = ["Zama "] packages = [ From d793bffc520979a849f9beb469a43405287bc0d5 Mon Sep 17 00:00:00 2001 From: youben11 Date: Wed, 8 Sep 2021 15:47:48 +0100 Subject: [PATCH 0220/1104] fix(typo): correct name of operation --- concrete/common/mlir/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 1af29dd75..cab84d863 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -173,7 +173,7 @@ def dot(node, preds, ir_to_mlir_node, ctx): ) ): raise TypeError( - f"Don't support subtraction between {type(node.inputs[0])} and {type(node.inputs[1])}" + f"Don't support dot between {type(node.inputs[0])} and {type(node.inputs[1])}" ) lhs_node, rhs_node = preds # need to flip as underlying operation need encrypted first From 845558d3a54c3ee00828539f93395ce0e6891c4a Mon Sep 17 00:00:00 2001 From: youben11 Date: Fri, 10 Sep 2021 11:49:32 +0100 Subject: [PATCH 0221/1104] test: dot compilation and execution --- tests/numpy/test_compile.py | 50 ++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index d9ac7b3fd..73f6e2388 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -9,7 +9,7 @@ from concrete.common.compilation import CompilationConfiguration from concrete.common.data_types.integers import Integer from concrete.common.debugging import draw_graph, get_printable_graph from concrete.common.extensions.table import LookupTable -from concrete.common.values import EncryptedScalar, EncryptedTensor +from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor from concrete.numpy.compile import ( compile_numpy_function, compile_numpy_function_into_op_graph, @@ -139,6 +139,54 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): assert compiler_engine.run(*args) == function(*args) +@pytest.mark.parametrize( + "size, input_range", + [ + pytest.param( + 1, + (0, 8), + ), + pytest.param( + 4, + (0, 8), + ), + pytest.param( + 8, + (0, 8), + ), + pytest.param( + 16, + (0, 4), + ), + ], +) +def test_compile_and_run_dot_correctness(size, input_range): + """Test correctness of results when running a compiled function""" + + def data_gen(input_range, size): + for i in range(*input_range, size): + vec = list(range(i, min(i + size, input_range[1]))) + yield vec, vec[::-1] + + function_parameters = { + "x": EncryptedTensor(Integer(64, False), (size,)), + "y": ClearTensor(Integer(64, False), (size,)), + } + + def function(x, y): + return numpy.dot(x, y) + + compiler_engine = compile_numpy_function( + function, + function_parameters, + data_gen(input_range, size), + ) + + low, high = input_range + args = [[random.randint(low, high) for _ in range(size)] for __ in range(2)] + assert compiler_engine.run(*args) == function(*args) + + def test_compile_function_with_direct_tlu(): """Test compile_numpy_function_into_op_graph for a program with direct table lookup""" From 860a7108964ed11c8e5f9228d52c6095ee7ac550 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 10 Sep 2021 15:32:49 +0200 Subject: [PATCH 0222/1104] fix(docker): do not use login shell in dev image --- docker/Dockerfile.concretefhe-dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile.concretefhe-dev b/docker/Dockerfile.concretefhe-dev index fb49959d3..9b3a6e1e9 100644 --- a/docker/Dockerfile.concretefhe-dev +++ b/docker/Dockerfile.concretefhe-dev @@ -13,4 +13,4 @@ RUN echo "source /${SRC_DIR_NAME}/.docker_venv/bin/activate" >> /root/.bashrc && WORKDIR /${SRC_DIR_NAME} -ENTRYPOINT ["/bin/bash", "-l"] +CMD ["/bin/bash"] From 585de7808143ca36dfc97b819c956ffbff23ce33 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 10 Sep 2021 15:35:05 +0200 Subject: [PATCH 0223/1104] fix(docker): update and fix release docker --- docker/Dockerfile.release | 13 ++++++------- docker/Dockerfile.release.dockerignore | 3 ++- docker/release_resources/entry_point.sh | 3 +++ .../release_requirements.txt | 0 4 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 docker/release_resources/entry_point.sh rename docker/{ => release_resources}/release_requirements.txt (100%) diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index 5b9577060..dfe518670 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -1,4 +1,4 @@ -FROM ghcr.io/zama-ai/zamalang-compiler:bb0126a86a36b9062760c97b7e2f9d7008549899 as builder +FROM ghcr.io/zama-ai/zamalang-compiler:967fda07a05b6a410fee2027514a7114bdf781e9 as builder RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ @@ -14,12 +14,12 @@ COPY pyproject.toml ./pyproject.toml RUN poetry build --format wheel -FROM ghcr.io/zama-ai/zamalang-compiler:bb0126a86a36b9062760c97b7e2f9d7008549899 +FROM ghcr.io/zama-ai/zamalang-compiler:967fda07a05b6a410fee2027514a7114bdf781e9 RUN mkdir /pkg && mkdir /app WORKDIR /pkg COPY --from=builder /build/dist/*.whl . -COPY docker/release_requirements.txt . +COPY docker/release_resources/release_requirements.txt . COPY torch_requirements.txt . RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ @@ -38,11 +38,10 @@ RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ python3 -m pip install --no-cache-dir -r release_requirements.txt WORKDIR /app -RUN printf "#!/bin/bash\npython3 -m jupyter notebook --ip=0.0.0.0 --allow-root --no-browser\n" \ - > entry_point.sh && \ - mkdir /data +COPY docker/release_resources/entry_point.sh ./entry_point.sh +RUN mkdir /data WORKDIR /data VOLUME [ "/data" ] -CMD ["/bin/bash", "-l", "/app/entry_point.sh"] +CMD ["/bin/bash", "-i", "/app/entry_point.sh"] diff --git a/docker/Dockerfile.release.dockerignore b/docker/Dockerfile.release.dockerignore index deffd550c..0108b388a 100644 --- a/docker/Dockerfile.release.dockerignore +++ b/docker/Dockerfile.release.dockerignore @@ -4,7 +4,8 @@ # Not our sources !concrete !pyproject.toml -!docker/release_requirements.txt +!docker/release_resources/entry_point.sh +!docker/release_resources/release_requirements.txt !torch_requirements.txt # But still ignore pycache diff --git a/docker/release_resources/entry_point.sh b/docker/release_resources/entry_point.sh new file mode 100644 index 000000000..33d5beffb --- /dev/null +++ b/docker/release_resources/entry_point.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python3 -m jupyter notebook --ip=0.0.0.0 --allow-root --no-browser diff --git a/docker/release_requirements.txt b/docker/release_resources/release_requirements.txt similarity index 100% rename from docker/release_requirements.txt rename to docker/release_resources/release_requirements.txt From e78086eefa4971a705127f223dc924cf525cb4d2 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 9 Sep 2021 18:16:37 +0200 Subject: [PATCH 0224/1104] feat: let's have a customizable assert closes #245 --- .../common/bounds_measurement/dataset_eval.py | 12 ++-- concrete/common/common_helpers.py | 8 ++- concrete/common/compilation/artifacts.py | 12 ++-- concrete/common/data_types/dtypes_helpers.py | 44 +++++++++----- concrete/common/data_types/floats.py | 3 +- concrete/common/data_types/integers.py | 3 +- concrete/common/debugging/__init__.py | 1 + concrete/common/debugging/custom_assert.py | 49 +++++++++++++++ concrete/common/debugging/drawing.py | 10 +++- concrete/common/debugging/printing.py | 9 +-- concrete/common/mlir/converters.py | 21 +++---- concrete/common/mlir/mlir_converter.py | 3 +- concrete/common/operator_graph.py | 60 ++++++++++++------- concrete/common/optimization/topological.py | 3 +- .../common/representation/intermediate.py | 22 ++++--- concrete/common/tracing/base_tracer.py | 9 +-- concrete/common/tracing/tracing_helpers.py | 3 +- concrete/numpy/np_dtypes_helpers.py | 47 +++++++++------ concrete/numpy/tracing.py | 32 ++++++---- tests/common/debugging/test_custom_assert.py | 29 +++++++++ 20 files changed, 262 insertions(+), 118 deletions(-) create mode 100644 concrete/common/debugging/custom_assert.py create mode 100644 tests/common/debugging/test_custom_assert.py diff --git a/concrete/common/bounds_measurement/dataset_eval.py b/concrete/common/bounds_measurement/dataset_eval.py index b47889f98..e8662f462 100644 --- a/concrete/common/bounds_measurement/dataset_eval.py +++ b/concrete/common/bounds_measurement/dataset_eval.py @@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, Iterator, Tuple +from ..debugging import custom_assert from ..operator_graph import OPGraph from ..representation.intermediate import IntermediateNode @@ -35,10 +36,13 @@ def eval_op_graph_bounds_on_dataset( """ def check_dataset_input_len_is_valid(data_to_check): - assert len(data_to_check) == len(op_graph.input_nodes), ( - f"Got input data from dataset of len: {len(data_to_check)}, " - f"function being evaluated has {len(op_graph.input_nodes)} inputs, please make " - f"sure your data generator returns valid tuples of input values" + custom_assert( + len(data_to_check) == len(op_graph.input_nodes), + ( + f"Got input data from dataset of len: {len(data_to_check)}, " + f"function being evaluated has {len(op_graph.input_nodes)} inputs, please make " + f"sure your data generator returns valid tuples of input values" + ), ) # TODO: do we want to check coherence between the input data type and the corresponding Input ir diff --git a/concrete/common/common_helpers.py b/concrete/common/common_helpers.py index 71dac78be..53b0989f8 100644 --- a/concrete/common/common_helpers.py +++ b/concrete/common/common_helpers.py @@ -3,6 +3,7 @@ from typing import List, Optional from .data_types.integers import Integer +from .debugging import custom_assert from .operator_graph import OPGraph from .representation import intermediate as ir @@ -53,9 +54,10 @@ def check_op_graph_is_integer_program( """ offending_nodes = [] if offending_nodes_out is None else offending_nodes_out - assert isinstance( - offending_nodes, list - ), f"offending_nodes_out must be a list, got {type(offending_nodes_out)}" + custom_assert( + isinstance(offending_nodes, list), + f"offending_nodes_out must be a list, got {type(offending_nodes_out)}", + ) offending_nodes.clear() offending_nodes.extend( diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index 152b48b62..f35347dd1 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, Optional, Union import networkx as nx from PIL import Image -from ..debugging import draw_graph, get_printable_graph +from ..debugging import custom_assert, draw_graph, get_printable_graph from ..operator_graph import OPGraph from ..representation import intermediate as ir from ..values import BaseValue @@ -102,7 +102,7 @@ class CompilationArtifacts: None """ - assert self.final_operation_graph is not None + custom_assert(self.final_operation_graph is not None) self.bounds_of_the_final_operation_graph = bounds def add_final_operation_graph_mlir(self, mlir: str): @@ -115,7 +115,7 @@ class CompilationArtifacts: None """ - assert self.final_operation_graph is not None + custom_assert(self.final_operation_graph is not None) self.mlir_of_the_final_operation_graph = mlir def export(self): @@ -186,7 +186,7 @@ class CompilationArtifacts: f.write(f"{representation}\n") if self.bounds_of_the_final_operation_graph is not None: - assert self.final_operation_graph is not None + custom_assert(self.final_operation_graph is not None) with open(output_directory.joinpath("bounds.txt"), "w", encoding="utf-8") as f: # TODO: # if nx.topological_sort is not deterministic between calls, @@ -194,11 +194,11 @@ class CompilationArtifacts: # thus, we may want to change this in the future for index, node in enumerate(nx.topological_sort(self.final_operation_graph.graph)): bounds = self.bounds_of_the_final_operation_graph.get(node) - assert bounds is not None + custom_assert(bounds is not None) f.write(f"%{index} :: [{bounds.get('min')}, {bounds.get('max')}]\n") if self.mlir_of_the_final_operation_graph is not None: - assert self.final_operation_graph is not None + custom_assert(self.final_operation_graph is not None) with open(output_directory.joinpath("mlir.txt"), "w", encoding="utf-8") as f: f.write(self.mlir_of_the_final_operation_graph) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 83b4a5084..49c096384 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -4,6 +4,7 @@ from copy import deepcopy from functools import partial from typing import Callable, Union, cast +from ..debugging.custom_assert import custom_assert from ..values import ( BaseValue, ClearScalar, @@ -149,8 +150,8 @@ def find_type_to_hold_both_lossy( Returns: BaseDataType: The dtype able to hold (potentially lossy) dtype1 and dtype2 """ - assert isinstance(dtype1, BASE_DATA_TYPES), f"Unsupported dtype1: {type(dtype1)}" - assert isinstance(dtype2, BASE_DATA_TYPES), f"Unsupported dtype2: {type(dtype2)}" + custom_assert(isinstance(dtype1, BASE_DATA_TYPES), f"Unsupported dtype1: {type(dtype1)}") + custom_assert(isinstance(dtype2, BASE_DATA_TYPES), f"Unsupported dtype2: {type(dtype2)}") type_to_return: BaseDataType @@ -208,8 +209,12 @@ def mix_scalar_values_determine_holding_dtype( value2 dtypes. """ - assert isinstance(value1, ScalarValue), f"Unsupported value1: {value1}, expected ScalarValue" - assert isinstance(value2, ScalarValue), f"Unsupported value2: {value2}, expected ScalarValue" + custom_assert( + isinstance(value1, ScalarValue), f"Unsupported value1: {value1}, expected ScalarValue" + ) + custom_assert( + isinstance(value2, ScalarValue), f"Unsupported value2: {value2}, expected ScalarValue" + ) holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) mixed_value: ScalarValue @@ -241,12 +246,19 @@ def mix_tensor_values_determine_holding_dtype( value2 dtypes. """ - assert isinstance(value1, TensorValue), f"Unsupported value1: {value1}, expected TensorValue" - assert isinstance(value2, TensorValue), f"Unsupported value2: {value2}, expected TensorValue" + custom_assert( + isinstance(value1, TensorValue), f"Unsupported value1: {value1}, expected TensorValue" + ) + custom_assert( + isinstance(value2, TensorValue), f"Unsupported value2: {value2}, expected TensorValue" + ) - assert value1.shape == value2.shape, ( - f"Tensors have different shapes which is not supported.\n" - f"value1: {value1.shape}, value2: {value2.shape}" + custom_assert( + value1.shape == value2.shape, + ( + f"Tensors have different shapes which is not supported.\n" + f"value1: {value1.shape}, value2: {value2.shape}" + ), ) holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) @@ -279,9 +291,10 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> dtypes. """ - assert ( - value1.__class__ == value2.__class__ - ), f"Cannot mix values of different types: value 1:{type(value1)}, value2: {type(value2)}" + custom_assert( + (value1.__class__ == value2.__class__), + f"Cannot mix values of different types: value 1:{type(value1)}, value2: {type(value2)}", + ) if isinstance(value1, ScalarValue) and isinstance(value2, ScalarValue): return mix_scalar_values_determine_holding_dtype(value1, value2) @@ -304,9 +317,10 @@ def get_base_data_type_for_python_constant_data(constant_data: Union[int, float] BaseDataType: The corresponding BaseDataType """ constant_data_type: BaseDataType - assert isinstance( - constant_data, (int, float) - ), f"Unsupported constant data of type {type(constant_data)}" + custom_assert( + isinstance(constant_data, (int, float)), + f"Unsupported constant data of type {type(constant_data)}", + ) if isinstance(constant_data, int): is_signed = constant_data < 0 constant_data_type = Integer( diff --git a/concrete/common/data_types/floats.py b/concrete/common/data_types/floats.py index 63b52b3b3..a26c240b8 100644 --- a/concrete/common/data_types/floats.py +++ b/concrete/common/data_types/floats.py @@ -2,6 +2,7 @@ from functools import partial +from ..debugging.custom_assert import custom_assert from . import base @@ -14,7 +15,7 @@ class Float(base.BaseDataType): def __init__(self, bit_width: int) -> None: super().__init__() - assert bit_width in (32, 64), "Only 32 and 64 bits floats are supported" + custom_assert(bit_width in (32, 64), "Only 32 and 64 bits floats are supported") self.bit_width = bit_width def __repr__(self) -> str: diff --git a/concrete/common/data_types/integers.py b/concrete/common/data_types/integers.py index 2cbe83560..181a017b1 100644 --- a/concrete/common/data_types/integers.py +++ b/concrete/common/data_types/integers.py @@ -3,6 +3,7 @@ import math from typing import Any, Iterable +from ..debugging.custom_assert import custom_assert from . import base @@ -14,7 +15,7 @@ class Integer(base.BaseDataType): def __init__(self, bit_width: int, is_signed: bool) -> None: super().__init__() - assert bit_width > 0, "bit_width must be > 0" + custom_assert(bit_width > 0, "bit_width must be > 0") self.bit_width = bit_width self.is_signed = is_signed diff --git a/concrete/common/debugging/__init__.py b/concrete/common/debugging/__init__.py index a5253afdc..c087039b5 100644 --- a/concrete/common/debugging/__init__.py +++ b/concrete/common/debugging/__init__.py @@ -1,3 +1,4 @@ """Module for debugging.""" +from .custom_assert import custom_assert from .drawing import draw_graph from .printing import get_printable_graph diff --git a/concrete/common/debugging/custom_assert.py b/concrete/common/debugging/custom_assert.py new file mode 100644 index 000000000..71c88512f --- /dev/null +++ b/concrete/common/debugging/custom_assert.py @@ -0,0 +1,49 @@ +"""Provide some variants of assert.""" + + +def custom_assert(condition: bool, on_error_msg: str = "") -> None: + """Provide a custom assert which is kept even if the optimized python mode is used. + + See https://docs.python.org/3/reference/simple_stmts.html#assert for the documentation + on the classical assert function + + Args: + condition(bool): the condition. If False, raise AssertionError + on_error_msg(str): optional message for precising the error, in case of error + + """ + + if not condition: + raise AssertionError(on_error_msg) + + +def assert_true(condition: bool, on_error_msg: str = ""): + """Provide a custom assert to check that the condition is True. + + Args: + condition(bool): the condition. If False, raise AssertionError + on_error_msg(str): optional message for precising the error, in case of error + + """ + return custom_assert(condition, on_error_msg) + + +def assert_false(condition: bool, on_error_msg: str = ""): + """Provide a custom assert to check that the condition is False. + + Args: + condition(bool): the condition. If True, raise AssertionError + on_error_msg(str): optional message for precising the error, in case of error + + """ + return custom_assert(not condition, on_error_msg) + + +def assert_not_reached(on_error_msg: str): + """Provide a custom assert to check that a piece of code is never reached. + + Args: + on_error_msg(str): message for precising the error + + """ + return custom_assert(False, on_error_msg) diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index a4662da8b..bcca75469 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -8,6 +8,7 @@ import matplotlib.pyplot as plt import networkx as nx from PIL import Image +from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph from ..representation import intermediate as ir from ..representation.intermediate import ALL_IR_NODES @@ -26,9 +27,12 @@ IR_NODE_COLOR_MAPPING = { } _missing_nodes_in_mapping = ALL_IR_NODES - IR_NODE_COLOR_MAPPING.keys() -assert len(_missing_nodes_in_mapping) == 0, ( - f"Missing IR node in IR_NODE_COLOR_MAPPING : " - f"{', '.join(sorted(str(node_type) for node_type in _missing_nodes_in_mapping))}" +custom_assert( + len(_missing_nodes_in_mapping) == 0, + ( + f"Missing IR node in IR_NODE_COLOR_MAPPING : " + f"{', '.join(sorted(str(node_type) for node_type in _missing_nodes_in_mapping))}" + ), ) del _missing_nodes_in_mapping diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 8cce142b1..3b3123f82 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -4,6 +4,7 @@ from typing import Any, Dict import networkx as nx +from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -32,7 +33,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: Returns: str: a string to print or save in a file """ - assert isinstance(opgraph, OPGraph) + custom_assert(isinstance(opgraph, OPGraph)) list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) graph = opgraph.graph @@ -46,7 +47,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: # This code doesn't work with more than a single output. For more outputs, # we would need to change the way the destination are created: currently, # they only are done by incrementing i - assert len(node.outputs) == 1 + custom_assert(len(node.outputs) == 1) if isinstance(node, ir.Input): what_to_print = node.input_name @@ -72,9 +73,9 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: list_of_arg_name += [(index["input_idx"], str(map_table[pred]))] # Some checks, because the previous algorithm is not clear - assert len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name)) + custom_assert(len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name))) list_of_arg_name.sort() - assert [x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name))) + custom_assert([x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name)))) # Then, just print the predecessors in the right order what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index cab84d863..20d7703e2 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -21,13 +21,14 @@ from ..data_types.dtypes_helpers import ( value_is_encrypted_scalar_unsigned_integer, value_is_encrypted_tensor_integer, ) +from ..debugging.custom_assert import custom_assert from ..representation import intermediate as ir def add(node, preds, ir_to_mlir_node, ctx): """Convert an addition intermediate node.""" - assert len(node.inputs) == 2, "addition should have two inputs" - assert len(node.outputs) == 1, "addition should have a single output" + custom_assert(len(node.inputs) == 2, "addition should have two inputs") + custom_assert(len(node.outputs) == 1, "addition should have a single output") if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): @@ -70,8 +71,8 @@ def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): def sub(node, preds, ir_to_mlir_node, ctx): """Convert a subtraction intermediate node.""" - assert len(node.inputs) == 2, "subtraction should have two inputs" - assert len(node.outputs) == 1, "subtraction should have a single output" + custom_assert(len(node.inputs) == 2, "subtraction should have two inputs") + custom_assert(len(node.outputs) == 1, "subtraction should have a single output") if value_is_clear_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_unsigned_integer( node.inputs[1] ): @@ -94,8 +95,8 @@ def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): def mul(node, preds, ir_to_mlir_node, ctx): """Convert a multiplication intermediate node.""" - assert len(node.inputs) == 2, "multiplication should have two inputs" - assert len(node.outputs) == 1, "multiplication should have a single output" + custom_assert(len(node.inputs) == 2, "multiplication should have two inputs") + custom_assert(len(node.outputs) == 1, "multiplication should have a single output") if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): @@ -134,8 +135,8 @@ def constant(node, _, __, ctx): def apply_lut(node, preds, ir_to_mlir_node, ctx): """Convert an arbitrary function intermediate node.""" - assert len(node.inputs) == 1, "LUT should have a single input" - assert len(node.outputs) == 1, "LUT should have a single output" + custom_assert(len(node.inputs) == 1, "LUT should have a single input") + custom_assert(len(node.outputs) == 1, "LUT should have a single output") if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): raise TypeError("Only support LUT with encrypted unsigned integers inputs") if not value_is_encrypted_scalar_unsigned_integer(node.outputs[0]): @@ -160,8 +161,8 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): def dot(node, preds, ir_to_mlir_node, ctx): """Convert a dot intermediate node.""" - assert len(node.inputs) == 2, "Dot should have two inputs" - assert len(node.outputs) == 1, "Dot should have a single output" + custom_assert(len(node.inputs) == 2, "Dot should have two inputs") + custom_assert(len(node.outputs) == 1, "Dot should have a single output") if not ( ( value_is_encrypted_tensor_integer(node.inputs[0]) diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index 352927ba7..2aa328ac3 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -25,6 +25,7 @@ from ..data_types.dtypes_helpers import ( value_is_encrypted_scalar_unsigned_integer, value_is_encrypted_tensor_unsigned_integer, ) +from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -93,7 +94,7 @@ class MLIRConverter: if is_signed and not is_encrypted: # clear signed return IntegerType.get_signed(bit_width) # should be clear unsigned at this point - assert not is_signed and not is_encrypted + custom_assert(not is_signed and not is_encrypted) # unsigned integer are considered signless in the compiler return IntegerType.get_signless(bit_width) diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index b19c42140..1c5ee104e 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -12,6 +12,7 @@ from .data_types.dtypes_helpers import ( ) from .data_types.floats import Float from .data_types.integers import Integer, make_integer_to_hold +from .debugging.custom_assert import custom_assert from .representation import intermediate as ir from .tracing import BaseTracer from .tracing.tracing_helpers import create_graph_from_output_tracers @@ -30,13 +31,17 @@ class OPGraph: input_nodes: Dict[int, ir.Input], output_nodes: Dict[int, ir.IntermediateNode], ) -> None: - assert len(input_nodes) > 0, "Got a graph without input nodes which is not supported" - assert all( - isinstance(node, ir.Input) for node in input_nodes.values() - ), "Got input nodes that were not ir.Input, which is not supported" - assert all( - isinstance(node, ir.IntermediateNode) for node in output_nodes.values() - ), "Got output nodes which were not ir.IntermediateNode, which is not supported" + custom_assert( + len(input_nodes) > 0, "Got a graph without input nodes which is not supported" + ) + custom_assert( + all(isinstance(node, ir.Input) for node in input_nodes.values()), + "Got input nodes that were not ir.Input, which is not supported", + ) + custom_assert( + all(isinstance(node, ir.IntermediateNode) for node in output_nodes.values()), + "Got output nodes which were not ir.IntermediateNode, which is not supported", + ) self.graph = graph self.input_nodes = input_nodes @@ -46,9 +51,10 @@ class OPGraph: def __call__(self, *args) -> Union[Any, Tuple[Any, ...]]: inputs = dict(enumerate(args)) - assert len(inputs) == len( - self.input_nodes - ), f"Expected {len(self.input_nodes)} arguments, got {len(inputs)} : {args}" + custom_assert( + len(inputs) == len(self.input_nodes), + f"Expected {len(self.input_nodes)} arguments, got {len(inputs)} : {args}", + ) results = self.evaluate(inputs) tuple_result = tuple(results[output_node] for output_node in self.get_ordered_outputs()) @@ -177,9 +183,12 @@ class OPGraph: min_data_type_constructor = get_type_constructor_for_constant_data(min_bound) max_data_type_constructor = get_type_constructor_for_constant_data(max_bound) - assert max_data_type_constructor == min_data_type_constructor, ( - f"Got two different type constructors for min and max bound: " - f"{min_data_type_constructor}, {max_data_type_constructor}" + custom_assert( + max_data_type_constructor == min_data_type_constructor, + ( + f"Got two different type constructors for min and max bound: " + f"{min_data_type_constructor}, {max_data_type_constructor}" + ), ) data_type_constructor = max_data_type_constructor @@ -191,20 +200,25 @@ class OPGraph: (min_bound, max_bound), force_signed=False ) else: - assert isinstance(min_data_type, Float) and isinstance( - max_data_type, Float - ), ( - "min_bound and max_bound have different common types, " - "this should never happen.\n" - f"min_bound: {min_data_type}, max_bound: {max_data_type}" + custom_assert( + isinstance(min_data_type, Float) and isinstance(max_data_type, Float), + ( + "min_bound and max_bound have different common types, " + "this should never happen.\n" + f"min_bound: {min_data_type}, max_bound: {max_data_type}" + ), ) output_value.data_type = Float(64) output_value.data_type.underlying_type_constructor = data_type_constructor else: # Currently variable inputs are only allowed to be integers - assert isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer), ( - f"Inputs to a graph should be integers, got bounds that were float, \n" - f"min: {min_bound} ({type(min_bound)}), max: {max_bound} ({type(max_bound)})" + custom_assert( + isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer), + ( + f"Inputs to a graph should be integers, got bounds that were float, \n" + f"min: {min_bound} ({type(min_bound)}), " + f"max: {max_bound} ({type(max_bound)})" + ), ) node.inputs[0].data_type = make_integer_to_hold( (min_bound, max_bound), force_signed=False @@ -215,7 +229,7 @@ class OPGraph: # TODO: #57 manage multiple outputs from a node, probably requires an output_idx when # adding an edge - assert len(node.outputs) == 1 + custom_assert(len(node.outputs) == 1) successors = self.graph.succ[node] for succ in successors: diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index 0638b1e16..7646c38b3 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -7,6 +7,7 @@ import networkx as nx from ..compilation.artifacts import CompilationArtifacts from ..data_types.floats import Float from ..data_types.integers import Integer +from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph from ..representation import intermediate as ir @@ -112,7 +113,7 @@ def convert_float_subgraph_to_fused_node( non_constant_start_nodes = [ node for node in float_subgraph_start_nodes if not isinstance(node, ir.Constant) ] - assert len(non_constant_start_nodes) == 1 + custom_assert(len(non_constant_start_nodes) == 1) current_subgraph_variable_input = non_constant_start_nodes[0] new_input_value = deepcopy(current_subgraph_variable_input.outputs[0]) diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index ed6325510..e42fc4b27 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -12,6 +12,7 @@ from ..data_types.dtypes_helpers import ( mix_scalar_values_determine_holding_dtype, ) from ..data_types.integers import Integer +from ..debugging.custom_assert import custom_assert from ..values import BaseValue, ClearScalar, EncryptedScalar, TensorValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" @@ -32,7 +33,7 @@ class IntermediateNode(ABC): **_kwargs, # This is to be able to feed arbitrary arguments to IntermediateNodes ) -> None: self.inputs = list(inputs) - assert all(isinstance(x, BaseValue) for x in self.inputs) + custom_assert(all(isinstance(x, BaseValue) for x in self.inputs)) # Register all IR nodes def __init_subclass__(cls, **kwargs): @@ -48,7 +49,7 @@ class IntermediateNode(ABC): """__init__ for a binary operation, ie two inputs.""" IntermediateNode.__init__(self, inputs) - assert len(self.inputs) == 2 + custom_assert(len(self.inputs) == 2) self.outputs = [mix_values_func(self.inputs[0], self.inputs[1])] @@ -147,7 +148,7 @@ class Input(IntermediateNode): program_input_idx: int, ) -> None: super().__init__((input_value,)) - assert len(self.inputs) == 1 + custom_assert(len(self.inputs) == 1) self.input_name = input_name self.program_input_idx = program_input_idx self.outputs = [deepcopy(self.inputs[0])] @@ -216,7 +217,7 @@ class ArbitraryFunction(IntermediateNode): op_kwargs: Optional[Dict[str, Any]] = None, ) -> None: super().__init__([input_base_value]) - assert len(self.inputs) == 1 + custom_assert(len(self.inputs) == 1) self.arbitrary_func = arbitrary_func self.op_args = op_args if op_args is not None else () self.op_kwargs = op_kwargs if op_kwargs is not None else {} @@ -295,12 +296,15 @@ class Dot(IntermediateNode): ] = default_dot_evaluation_function, ) -> None: super().__init__(inputs) - assert len(self.inputs) == 2 + custom_assert(len(self.inputs) == 2) - assert all( - isinstance(input_value, TensorValue) and input_value.ndim == 1 - for input_value in self.inputs - ), f"Dot only supports two vectors ({TensorValue.__name__} with ndim == 1)" + custom_assert( + all( + isinstance(input_value, TensorValue) and input_value.ndim == 1 + for input_value in self.inputs + ), + f"Dot only supports two vectors ({TensorValue.__name__} with ndim == 1)", + ) output_scalar_value = ( EncryptedScalar diff --git a/concrete/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py index dfaa12548..094c8f80d 100644 --- a/concrete/common/tracing/base_tracer.py +++ b/concrete/common/tracing/base_tracer.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Iterable, List, Tuple, Type, Union +from ..debugging.custom_assert import custom_assert from ..representation import intermediate as ir from ..representation.intermediate import IR_MIX_VALUES_FUNC_ARG_NAME from ..values import BaseValue @@ -105,7 +106,7 @@ class BaseTracer(ABC): ir.Add, ) - assert len(result_tracer) == 1 + custom_assert(len(result_tracer) == 1) return result_tracer[0] # With that is that x + 1 and 1 + x have the same graph. If we want to keep @@ -122,7 +123,7 @@ class BaseTracer(ABC): ir.Sub, ) - assert len(result_tracer) == 1 + custom_assert(len(result_tracer) == 1) return result_tracer[0] def __rsub__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": @@ -134,7 +135,7 @@ class BaseTracer(ABC): ir.Sub, ) - assert len(result_tracer) == 1 + custom_assert(len(result_tracer) == 1) return result_tracer[0] def __mul__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": @@ -146,7 +147,7 @@ class BaseTracer(ABC): ir.Mul, ) - assert len(result_tracer) == 1 + custom_assert(len(result_tracer) == 1) return result_tracer[0] # With that is that x * 3 and 3 * x have the same graph. If we want to keep diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index bf8748fe2..712926270 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -6,6 +6,7 @@ from typing import Callable, Dict, Iterable, OrderedDict, Set, Type import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph +from ..debugging.custom_assert import custom_assert from ..representation import intermediate as ir from ..values import BaseValue from .base_tracer import BaseTracer @@ -121,6 +122,6 @@ def create_graph_from_output_tracers( current_tracers = next_tracers - assert is_directed_acyclic_graph(graph) + custom_assert(is_directed_acyclic_graph(graph)) return graph diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 1158925c4..2d2c4eab7 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -16,6 +16,7 @@ from ..common.data_types.dtypes_helpers import ( ) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer +from ..common.debugging.custom_assert import custom_assert from ..common.values import BaseValue, ScalarValue, TensorValue NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { @@ -69,16 +70,20 @@ def convert_base_data_type_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.d Returns: numpy.dtype: The resulting numpy.dtype """ - assert isinstance( - common_dtype, BASE_DATA_TYPES - ), f"Unsupported common_dtype: {type(common_dtype)}" + custom_assert( + isinstance(common_dtype, BASE_DATA_TYPES), f"Unsupported common_dtype: {type(common_dtype)}" + ) type_to_return: numpy.dtype if isinstance(common_dtype, Float): - assert common_dtype.bit_width in ( - 32, - 64, - ), "Only converting Float(32) or Float(64) is supported" + custom_assert( + common_dtype.bit_width + in ( + 32, + 64, + ), + "Only converting Float(32) or Float(64) is supported", + ) type_to_return = ( numpy.dtype(numpy.float64) if common_dtype.bit_width == 64 @@ -110,9 +115,10 @@ def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> BaseDataType: The corresponding BaseDataType """ base_dtype: BaseDataType - assert isinstance( - constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) - ), f"Unsupported constant data of type {type(constant_data)}" + custom_assert( + isinstance(constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)), + f"Unsupported constant data of type {type(constant_data)}", + ) if isinstance(constant_data, (numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)): # numpy base_dtype = convert_numpy_dtype_to_base_data_type(constant_data.dtype) @@ -141,9 +147,10 @@ def get_base_value_for_numpy_or_python_constant_data( with `encrypted` as keyword argument (forwarded to the BaseValue `__init__` method). """ constant_data_value: Callable[..., BaseValue] - assert isinstance( - constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) - ), f"Unsupported constant data of type {type(constant_data)}" + custom_assert( + isinstance(constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)), + f"Unsupported constant data of type {type(constant_data)}", + ) base_dtype = get_base_data_type_for_numpy_or_python_constant_data(constant_data) if isinstance(constant_data, numpy.ndarray): @@ -171,9 +178,10 @@ def get_numpy_function_output_dtype( List[numpy.dtype]: The ordered numpy dtypes of the function outputs """ if isinstance(function, numpy.ufunc): - assert ( - len(input_dtypes) == function.nin - ), f"Expected {function.nin} types, got {len(input_dtypes)}: {input_dtypes}" + custom_assert( + (len(input_dtypes) == function.nin), + f"Expected {function.nin} types, got {len(input_dtypes)}: {input_dtypes}", + ) input_numpy_dtypes = [convert_base_data_type_to_numpy_dtype(dtype) for dtype in input_dtypes] @@ -203,9 +211,10 @@ def get_type_constructor_for_numpy_or_python_constant_data(constant_data: Any): constant_data (Any): The data for which we want to determine the type constructor. """ - assert isinstance( - constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) - ), f"Unsupported constant data of type {type(constant_data)}" + custom_assert( + isinstance(constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)), + f"Unsupported constant data of type {type(constant_data)}", + ) scalar_constructor: Type diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 0c1739852..7376a63f0 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -7,6 +7,7 @@ import numpy from numpy.typing import DTypeLike from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype +from ..common.debugging.custom_assert import custom_assert from ..common.operator_graph import OPGraph from ..common.representation.intermediate import ArbitraryFunction, Constant, Dot from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters @@ -40,9 +41,10 @@ class NPTracer(BaseTracer): """ if method == "__call__": tracing_func = self.get_tracing_func_for_np_function(ufunc) - assert ( - len(kwargs) == 0 - ), f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc}" + custom_assert( + (len(kwargs) == 0), + f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc}", + ) return tracing_func(*input_tracers, **kwargs) raise NotImplementedError("Only __call__ method is supported currently") @@ -52,9 +54,10 @@ class NPTracer(BaseTracer): Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch """ tracing_func = self.get_tracing_func_for_np_function(func) - assert ( - len(kwargs) == 0 - ), f"**kwargs are currently not supported for numpy functions, func: {func}" + custom_assert( + (len(kwargs) == 0), + f"**kwargs are currently not supported for numpy functions, func: {func}", + ) return tracing_func(*args, **kwargs) def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": @@ -69,10 +72,13 @@ class NPTracer(BaseTracer): Returns: NPTracer: The NPTracer representing the casting operation """ - assert len(args) == 0, f"astype currently only supports tracing without *args, got {args}" - assert ( - len(kwargs) == 0 - ), f"astype currently only supports tracing without **kwargs, got {kwargs}" + custom_assert( + len(args) == 0, f"astype currently only supports tracing without *args, got {args}" + ) + custom_assert( + (len(kwargs) == 0), + f"astype currently only supports tracing without **kwargs, got {kwargs}", + ) normalized_numpy_dtype = numpy.dtype(numpy_dtype) output_dtype = convert_numpy_dtype_to_base_data_type(numpy_dtype) @@ -139,9 +145,9 @@ class NPTracer(BaseTracer): Returns: NPTracer: The output NPTracer containing the traced function """ - assert len(input_tracers) == 1 + custom_assert(len(input_tracers) == 1) common_output_dtypes = cls._manage_dtypes(unary_operator, *input_tracers) - assert len(common_output_dtypes) == 1 + custom_assert(len(common_output_dtypes) == 1) traced_computation = ArbitraryFunction( input_base_value=input_tracers[0].output, @@ -167,7 +173,7 @@ class NPTracer(BaseTracer): dot_inputs = (self, self._sanitize(other_tracer)) common_output_dtypes = self._manage_dtypes(numpy.dot, *dot_inputs) - assert len(common_output_dtypes) == 1 + custom_assert(len(common_output_dtypes) == 1) traced_computation = Dot( [input_tracer.output for input_tracer in dot_inputs], diff --git a/tests/common/debugging/test_custom_assert.py b/tests/common/debugging/test_custom_assert.py new file mode 100644 index 000000000..aa34b7a85 --- /dev/null +++ b/tests/common/debugging/test_custom_assert.py @@ -0,0 +1,29 @@ +"""Test custom assert functions.""" +import pytest + +from concrete.common.debugging.custom_assert import ( + assert_false, + assert_not_reached, + assert_true, +) + + +def test_assert_not_functions(): + """Test custom assert functions""" + assert_true(True, "one check") + assert_false(False, "another check") + + with pytest.raises(AssertionError) as excinfo: + assert_not_reached("yet another one") + + assert "yet another one" in str(excinfo.value) + + with pytest.raises(AssertionError) as excinfo: + assert_true(False, "one failing check") + + assert "one failing check" in str(excinfo.value) + + with pytest.raises(AssertionError) as excinfo: + assert_false(True, "another failing check") + + assert "another failing check" in str(excinfo.value) From cd53e233c62ea89dd5fbe25d75b5b9e936998e7e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 13 Sep 2021 09:31:28 +0200 Subject: [PATCH 0225/1104] build: add a weekly trigger, only run notebooks during weekly - may need to adapt the notebook timeouts --- .github/workflows/continuous-integration.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 947fb282a..a52f1c8e8 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -10,6 +10,12 @@ on: types: - env-docker-preflight + schedule: + # * is a special character in YAML so you have to quote this string + # At 22:00 on Sunday + # Timezone is UTC, so Paris time is +2 during the summer and +1 during winter + - cron: '0 22 * * 0' + jobs: build: concurrency: @@ -80,7 +86,7 @@ jobs: run: | make pytest - name: Notebooks - if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + if: ${{ github.event_name == 'schedule' && steps.conformance.outcome == 'success' && !cancelled() }} env: # TODO: remove this when JIT doesn't need this LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so From a0a1b59a247a56dcb04d24994bd3c908454706a6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 13 Sep 2021 09:50:21 +0200 Subject: [PATCH 0226/1104] fix: add flag +x to docker/build_release_image.sh --- docker/build_release_image.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/build_release_image.sh diff --git a/docker/build_release_image.sh b/docker/build_release_image.sh old mode 100644 new mode 100755 From fb2b7eb00335eee387d16fcb25494f28e450cb75 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 13 Sep 2021 09:51:14 +0200 Subject: [PATCH 0227/1104] chore: udpate release template --- .github/ISSUE_TEMPLATE/release.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index dc7f0c6c3..9f456f1b1 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -9,12 +9,12 @@ Release check-list: - [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Zrc?` for Release Candidates) following semantic versioning: https://semver.org/ - [ ] Update the version in pyproject.toml to `X.Y.Z` (or `X.Y.Zrc?`) - [ ] Check the release milestone issues, cut out what can't be completed in time -- [ ] Checkout the commit for release, create a signed tag with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Zrc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Zrc?`) -- [ ] Run sanity checks inside the dev docker: `make pcc` and `make pytest && make coverage` +- [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Zrc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Zrc?`) +- [ ] Run sanity checks inside the dev docker: `make docker_build_and_start`, `make pcc` and `make pytest && make coverage` - [ ] On the build machine with docker installed, run in your OS terminal in the project dir: `make release_docker` -- [ ] Re-tag the image with `docker tag concretefhe-release:latest ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` (or `vX.Y.Zrc?`) +- [ ] Re-tag the image with `docker tag concretefhe:latest ghcr.io/zama-ai/concretefhe:vX.Y.Z` (or `vX.Y.Zrc?`) - [ ] `docker login ghcr.io`, input your username and GitHub Personal Access Token (PAT). If not already done add `write:packages` to your PAT -- [ ] Push the release image `docker push ghcr.io/zama-ai/concretefhe-release:vX.Y.Z` (or `vX.Y.Zrc?`) -- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link \(`ghcr.io/zama-ai/concretefhe-release:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image +- [ ] Push the release image `docker push ghcr.io/zama-ai/concretefhe:vX.Y.Z` (or `vX.Y.Zrc?`) +- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image All done! From b7a7d3d064c356fb60d1cca2e3362aa3452829e3 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 10 Sep 2021 14:26:56 +0300 Subject: [PATCH 0228/1104] feat: make the inference in examples homomorphic --- .github/workflows/continuous-integration.yaml | 2 - Makefile | 4 - examples/QuantizedLinearRegression.ipynb | 540 ++++++++------- examples/QuantizedLogisticRegression.ipynb | 634 ++++++++++-------- script/nbmake_utils/notebook_test_timeout.py | 23 - 5 files changed, 670 insertions(+), 533 deletions(-) delete mode 100644 script/nbmake_utils/notebook_test_timeout.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index a52f1c8e8..3de54834e 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -91,8 +91,6 @@ jobs: # TODO: remove this when JIT doesn't need this LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | - make strip_nb - make notebook_timeout make pytest_nb - name: Test coverage id: coverage diff --git a/Makefile b/Makefile index 297628334..4789f9bb8 100644 --- a/Makefile +++ b/Makefile @@ -160,10 +160,6 @@ strip_nb: poetry run python ./script/nbmake_utils/notebook_sanitize.py examples .PHONY: strip_nb -notebook_timeout: - poetry run python ./script/nbmake_utils/notebook_test_timeout.py examples -.PHONY: notebook_timeout - pytest_nb: poetry run pytest --nbmake examples/*.ipynb .PHONY: pytest_nb diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index bfae7b759..c6ef9331a 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -2,113 +2,129 @@ "cells": [ { "cell_type": "markdown", + "id": "73e4f53d", + "metadata": {}, "source": [ "# Quantized Linear Regression\n", "\n", "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "15e6e686", + "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our linear regression model" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 1, + "id": "0c2101de", + "metadata": {}, + "outputs": [], "source": [ "import numpy as np" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "6f02c3d4", + "metadata": {}, "source": [ "### And some helpers for visualization" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 2, + "id": "91260335", + "metadata": {}, + "outputs": [], "source": [ + "%matplotlib inline\n", + "\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "27f67e43", + "metadata": {}, "source": [ "### We need a dataset, a handcrafted one for simplicity" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 3, + "id": "84b42c42", + "metadata": {}, + "outputs": [], "source": [ "x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", "y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "fba2eecb", + "metadata": {}, "source": [ "### Let's visualize our dataset to get a grasp of it" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 4, + "id": "a8c83085", + "metadata": {}, + "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 5, + "id": "93e61f29", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "fd40fedf", + "metadata": {}, "source": [ "### Now, we need a model so let's define it\n", "\n", "The main purpose of this tutorial is not to train a linear regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 6, + "id": "4f95ae45", + "metadata": {}, + "outputs": [], "source": [ "class Model:\n", " w = None\n", @@ -130,145 +146,160 @@ "\n", " def evaluate(self, x):\n", " return x @ self.w + self.b" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "9b0c8d49", + "metadata": {}, "source": [ "### And create one" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 7, + "id": "ad97e3e0", + "metadata": {}, + "outputs": [], "source": [ "model = Model().fit(x, y)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "e18b52fd", + "metadata": {}, "source": [ "### Time to make some predictions" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 8, + "id": "5fd2e6bf", + "metadata": {}, + "outputs": [], "source": [ "inputs = np.linspace(40, 210, 100).reshape(-1, 1)\n", "predictions = model.evaluate(inputs)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "fd49b135", + "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 9, + "id": "e76b0343", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "ax.plot(inputs, predictions, color=\"blue\")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "0e080f5b", + "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 10, - "source": [ - "print(model.w)\n", - "print(model.b)" - ], + "id": "32ebe574", + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "[[2.669915]]\n", "-3.2335143\n" ] } ], - "metadata": {} + "source": [ + "print(model.w)\n", + "print(model.b)" + ] }, { "cell_type": "markdown", + "id": "b1b90d66", + "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b3e45e1f", + "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 11, + "id": "7d878724", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from IPython.display import SVG\n", "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ], - "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": [ - "" - ], - "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" - }, - "metadata": {}, - "execution_count": 11 - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "814afccd", + "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 12, + "id": "d81af434", + "metadata": {}, + "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -401,59 +432,65 @@ " domain = np.array(range(2**input_bits), dtype=np.uint)\n", " table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", " return QuantizedFunction(table)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "0ddd6342", + "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 13, + "id": "9189e38d", + "metadata": {}, + "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(model.w, parameter_bits)\n", "b_q = QuantizedArray.of(model.b, parameter_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "add5b6c2", + "metadata": {}, "source": [ "### And quantize our inputs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 14, + "id": "e1f94ff2", + "metadata": {}, + "outputs": [], "source": [ "input_bits = 6\n", "\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "37209a62", + "metadata": {}, "source": [ "### Time to make quantized inference" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 15, + "id": "131be184", + "metadata": {}, + "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -462,48 +499,52 @@ "y_q = x_q.affine(w_q, b_q, min_y, max_y, output_bits)\n", "\n", "quantized_predictions = y_q.dequantize()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "ea94c049", + "metadata": {}, "source": [ "### And visualize the results" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 16, + "id": "2ab0f580", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "ax.plot(inputs, quantized_predictions, color=\"black\")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "ea85b0ea", + "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 17, + "id": "2d341f26", + "metadata": {}, + "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (max_y - min_y)\n", "zp_y = int(round(min_y * q_y))\n", @@ -519,12 +560,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b6c4b6c0", + "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -541,28 +582,32 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "907fc5b1", + "metadata": {}, "source": [ "### Let's import the concrete numpy package now!" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "import concrete.numpy as hnp" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 18, + "id": "15e7e265", + "metadata": {}, + "outputs": [], + "source": [ + "import concrete.numpy as hnp" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "85034b43", + "metadata": {}, + "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -578,20 +623,22 @@ "\n", "def infer(x_0):\n", " return table[(x_0 + zp_x) * w_0]" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "91d4f22b", + "metadata": {}, "source": [ - "### Time to compile our quantized inference function" - ], - "metadata": {} + "### Let's compile our quantized inference function to it's operation graph for visualization" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, + "id": "d6bc9eee", + "metadata": {}, + "outputs": [], "source": [ "dataset = []\n", "for x_i in x_q:\n", @@ -602,118 +649,143 @@ " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", " iter(dataset),\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "2177fbd9", + "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 20, - "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "\n", - "%0 = Constant(1) # Integer\n", - "%1 = x_0 # Integer\n", - "%2 = Constant(15) # Integer\n", - "%3 = Add(1, 2) # Integer\n", - "%4 = Mul(3, 0) # Integer\n", - "%5 = TLU(4) # Integer\n", - "return(%5)\n" - ] - } - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 21, - "source": [ - "hnp.draw_graph(homomorphic_model).show()" - ], + "id": "e284fcc3", + "metadata": {}, "outputs": [ { - "output_type": "display_data", - "data": { - "text/plain": [ - "" - ], - "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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" - }, - "metadata": {} + "name": "stdout", + "output_type": "stream", + "text": [ + "%0 = Constant(1) # ClearScalar>\n", + "%1 = x_0 # EncryptedScalar>\n", + "%2 = Constant(15) # ClearScalar>\n", + "%3 = Add(1, 2) # EncryptedScalar>\n", + "%4 = Mul(3, 0) # EncryptedScalar>\n", + "%5 = TLU(4) # EncryptedScalar>\n", + "return(%5)\n", + "\n" + ] } ], - "metadata": {} - }, - { - "cell_type": "markdown", "source": [ - "### Finally, it's time to make homomorphic inference\n", - "\n", - "Or, at least, simulate it until the compiler integration is complete." - ], - "metadata": {} + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ] }, { "cell_type": "code", "execution_count": 22, - "source": [ - "homomorphic_predictions = []\n", - "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", - " evaluation = homomorphic_model.evaluate({0: x_i})\n", - " inference = QuantizedArray(evaluation[homomorphic_model.output_nodes[0]], y_q.parameters)\n", - " homomorphic_predictions.append(inference.dequantize())\n", - "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" + "id": "bee209f2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } ], - "outputs": [], - "metadata": {} + "source": [ + "hnp.draw_graph(homomorphic_model).show()" + ] }, { "cell_type": "markdown", + "id": "759bc39c", + "metadata": {}, "source": [ - "### And visualize it" - ], - "metadata": {} + "### It's time to compile the function to its homomorphic equivalent" + ] }, { "cell_type": "code", "execution_count": 23, + "id": "5c62c8b2", + "metadata": {}, + "outputs": [], "source": [ - "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", - "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + "engine = hnp.compile_numpy_function(\n", + " infer,\n", + " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", + " iter(dataset),\n", + ")" + ] }, { "cell_type": "markdown", + "id": "2d6865f7", + "metadata": {}, + "source": [ + "### Finally, let's make homomorphic inference" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "29374aa5", + "metadata": {}, + "outputs": [], + "source": [ + "homomorphic_predictions = []\n", + "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", + " inference = QuantizedArray(engine.run(x_i), y_q.parameters)\n", + " homomorphic_predictions.append(inference.dequantize())\n", + "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" + ] + }, + { + "cell_type": "markdown", + "id": "420b654c", + "metadata": {}, + "source": [ + "### And visualize it" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "1fc3156e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAD4CAYAAAAXUaZHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAm0ElEQVR4nO3deXgUVb7G8e9J2ARMAgSRfc3CoiIwouOGMiq4gFxcL+Ogg8IoaowogggEFUZ0FOMVRNxxAREFl1EGRXGZES/B68ggIiAEkkBCEkJ2QpJz/6iKJjGQQBKq0/1+nqefVJ2q7vzop309OXX6lLHWIiIi/iXI6wJERKTuKdxFRPyQwl1ExA8p3EVE/JDCXUTEDzXyugCA8PBw261bN6/LEBFpUDZs2JBurW1b1TGfCPdu3bqRkJDgdRkiIg2KMSbxcMc0LCMi4ocU7iIifkjhLiLihxTuIiJ+SOEuIuKHFO4iIn5I4S4i4ocU7iIiHsjKLqTDjYP44Itv6uX1Fe4iIsfZ2i+KaDs+mj3dNxD3xlP18jt84huqIiKBIDcXJk8p4pmU3nBaIgP2X0zCwtfr5Xcp3EVE6lFuQS73v3o/P2zL4ut1kN9uLZy2m/NLhrL2yX/U2+9VuIuI1JP8wnx6TY0ktdUeaAEMddov4AI+ffCTev3dCncRkXpQWFRI59hIMk/eA/8YwU1nTGfcOAgPO5GozlH1/vsV7iIidWznrkL6PBBJQc9kQr8eyWfzV3L66ce3BoW7iEgdsRZeeLGICauiKe23m+hdl7Hxg5U08iBpFe4iInVg5064ZXwRn7R0ZsKcU3gJX77wgWf1KNxFRI5RaWkpq9b/gxXvFfHqq1A06B447WeGmj/wyV9XeVqbwl1E5BjkF+bTfXIkaW2SoQkwzmm/gAv4ZMbHntYGCncRkaOWk1dIp5gosjsnE/zPc7jolN8R3Ru6te1KzMgYr8sDFO4iIkflX+sKGfJUJIeikujw/Ui+fXUl7dp5XdVvKdxFRGqgoACmzyzi8e3RcOpuBmZcRsLbK70u67C0cJiISDW+/BJO7V/E49ui4dREhtphJDzlzoSx1tviDkPhLiJyGNnZMHEinHd+ETtP7QOn7eCinzrzycwPnROshdhYiIvztM6qKNxFRCrJL8yn/W09CX3EsCDUwNSmFPfbzoVbO7H6jd1OoJcFe3w8ZGX5XA9eY+4iIuXsTs4nalokBd2TabypJz3ah9KiBQwMH8iiGc9CuBvo8fHOE2JiYN48MMbbwisx1gf+bzNo0CCbkJDgdRkiEsCshdeXFDL23UhK++wmasdI/v3sSpo2reLEoHKDHqWlngW7MWaDtXZQVcc0LCMiAS8lBa4cVcQN70RT2mc35xVexo8vHybYY2MrtpUN0fgYhbuIBCxr4YUXILpPEe8FR8MpiVwSPIzP/1rFmjDlx9hjYpwee0yMs++DAa8xdxEJSD//DOPHw5pPi2l2bR+I3sFFQRex6oGPqn6CMRAWVnGMfd4851hYmMbcq6IxdxGpFWsrhmvl/XJy8vLpE3MeSQd3AtAktICitvlcaC5kzYw1dfq76lutx9yNMTuNMRuNMd8ZYxLcttbGmI+NMVvdn63cdmOMecoYs80Y870xZkDd/VNERCqJi6s4LHKEuefrv82nzYRIkjpvILhlAc1bF9K4sWFE0xE1C3b4bZD7WI+9zNEMy1xgrU0vtz8FWGOtfcQYM8Xdvw8YDkS4j8HAM+5PEZG6Za0zx7xsWuK8eRXHxd1edVERPDS7kId/iIZ+yQzYN5KEBSt9NZfrRG3G3EcCQ9ztV4C1OOE+ElhsnfGedcaYMGNMe2vtntoUKiLyG+XHvSvNPc9+eBYjZl3Azoy97NkLRS33Qr8DXGQvY/XTKz0r+Xip6WwZC6w2xmwwxox329qVC+y9QNm6aB2B3eWem+S2VWCMGW+MSTDGJOzbt+8YShcRoWLAu3LnPETEA1F8bj4nselWirpsJahNLiObjmR1nHd3RzqeatpzP8dam2yMOQn42BjzY/mD1lprjDmqK7PW2kXAInAuqB7Nc0VEflFp7nluMHS9rROZ3bPhg+sYP2AJjz4KoaEe1uiBGvXcrbXJ7s80YAVwBpBqjGkP4P5Mc09PBjqXe3ont01EpG5Vmnu+Z08u7a5rQ2b3bFqsHsGnc9/g2WcDL9ihBuFujGlhjDmxbBu4GPgP8B4w1j1tLPCuu/0e8Cd31syZwAGNt4tIvSg393z5ebPpFBtFfkQGEV+fQ9qQM7jgQj++YlqNmgzLtANWGOeyciPgDWvtKmPMemCZMWYckAhc457/IXApsA3IB26q86pFRFz7JsYx8Y4C3loSBf2SOb/gStZ+9I7PTlE8XqoNd2vtz8BpVbRnAEOraLfAxDqpTkSkCtl52Ux/fTrf/ZDNN9/AwS6roV8Klza6nL8/ssLr8nyClh8QkQYlOy+bnvdHkN46DVoBw5z2YY2G8fdp73tamy9RuItIg5Gdl0vnSVFkt08j6B+juf0Pk7j6amgTGkbvLr29Ls+nKNxFpEH4flM+v5sbSVHPvZy0/mr+9eoyevb0uirfpXAXEZ9WXAyP/i2faf8XCX32MCB1NAnvLwv066XV0nruIuKzNm6EwWcVMm1DNPRJ5mI7kg0LlivYa0A9dxHxKaWlpaz63zW89sYh3nwTOP826Lubyxpfxgf3r/S6vAZD4S4iPiM7L5vu90WS2TYV2gC3Oe3DGg3jg/sDY02YuqJwFxGfkJqeS48pUeR3TqXJ1+dzycBT6dYVIk6O4I4Rd3hdXoOjcBcRz/39o3xGvhZJSeReem65mm+XLSMkxOuqGjaFu4h4JisL7ro7n1dyI6HvHs7PG83aN5Z5XZZf0GwZEfHEypUQ3aeQV3KioW8yVzS+krWPLve6LL+hcBeR4yo1Fa65BkaNLiRzSBT0283lTS7nvfu1Jkxd0rCMiBwXB3Kz6TnpVDJa7YIewH2WQ01heKPhvD9Va8LUNYW7iNS7H7bkcvpfoyjqvpemm3vSrX1LmjWD37f7PQv+ssDr8vySwl1E6k1pKfzP/Hzu+lckRO/l9JRrWf/6UoKDva7M/yncRaROZWZn8rsHf0dScQqHDoFtXAzRxQyzo/no2aVelxcwFO4iUmeycrOInBFJRlgGbD0JUxpMaChc1eYynr/9Oa/LCygKdxGpE9l52fSYGsn+8Ax490ZGdXuJ+fOhfXuvKwtMCncRqbW0jGy6T4kgv9M+mq6+gdenvcTo0V5XFdgU7iJSK598lsuwF6Mo6ZVG903Xk/D+Ylq39roqUbiLyDHJzYV77svn2YxI6L2Xc3Ou5otlb3hdlrgU7iJSY5nZmYx6YhQ79u1jTwoUh6VA7wOMbDKalX/TmjC+ROEuIjWSlZtFr+mR7G+VAc2DoJezfsnoFlez7B4Fu69RuItItbJys+gyOYKcds5MmKnDXmLGDGjWzOvK5HAU7iJyRD/9nM2pD0dxsGs6rb78E5+++BL9+3tdlVRH4S4iFVkLxmAtPPt8LretjcRGpnHa7utZ/9ErNG7sdYFSEwp3EflVXBxkZbEzZh7jJhTwaetI6J3K8C2n8+EbmgnTkCjcRQSArJz9zNz6Hus3HeR/vxhHSfRHELWX0R/C8rPO+6VHLw2Dwl1EyMrNovv9EWRFZkAkwA9g4cpVsPysGJg3T8HewCjcRQLcvv1ZdJsSQX77DBqvvp57r5zAxXOGEH4I+uYB/1KwN0S6zZ5IAPv8n9m0j4kiv0M6Xb8fS9I7rzM7eQXnZ7nBDhAb6wzJSIOicBcJQAUFcPe9uQyZH0lJzzTOzx7Dzrdf4qS/xkJ8PMTEOHfaiIlx9hXwDY6GZUQCzJdfwk3j8tk+wJkJM6rJtbzz+GvOwbAwJ9DLxtjnzfu1XUMzDYqxPvB/40GDBtmEhASvyxDxW8UlxXy07gsWLSrhg7+XEnzxOEqikrmq+VW8de9bFU+uPCtGs2R8ljFmg7V2UFXH1HMXaQhqEbhZuVl0va8X2SdlQA/gDigBRp0w6rfBDr99XQV7g1TjcDfGBAMJQLK19nJjTHdgKdAG2ADcYK0tMsY0BRYDA4EM4Fpr7c46r1wkULhfLPplqMRaZww8LMw5dgTbEw/Q98EIDnbOoPk3FzBscDTt2kGfjn24/Yrbj0Px4pWj6bnHAJuBEHd/LjDPWrvUGLMQGAc84/7cb63tZYy5zj3v2jqsWSRwWOsEe3y8sz9vnhPsZRc9K/XgrbUUFhZiLbz+Zi4T1vTDRqRz2s6xfLPiZZo29eafIcdfjWbLGGM6AZcBz7v7BrgQWO6e8gpwpbs90t3HPT7UPV9EjlbZRc2yWStBQb8Ge6UvFuXl5TFs2DCaN29Oi5DmjF99EjYijctKx/DdSwr2QFPTqZBPApOBUne/DZBlrS1295OAju52R2A3gHv8gHt+BcaY8caYBGNMwr59+46tepFAUH7WSplKwZ6fn88VV1zBxx9/QuNmkzBX94FouK3r7Xww67XjXLD4gmrD3RhzOZBmrd1Ql7/YWrvIWjvIWjuobdu2dfnSIv6lbIy9vHLzzgsKCrj44pF89tlarHmB0D/vwEb/wNPDn2b+jf/jQcHiC2oy5n42MMIYcynQDGfMPR4IM8Y0cnvnnYBk9/xkoDOQZIxpBITiXFgVkaNVFuzlh2LK9oGMuJl0n9SbnLNT4eyWNGsRQ7rNJn5YPBPPmOhx8eKlasPdWjsVmApgjBkC3GOtHWOMeQu4CmfGzFjgXfcp77n7X7vHP7W+MJlepCEy5rBfLFpX0JFz74qkuFs6rXadzYhh/WnZEs7pcg7X9bvO27rFc7WZ534fsNQY8zDwf8ALbvsLwKvGmG1AJqBPmUhtxMVVmBVTdMgwK2QWc/ZFQGQ65+4fy+cvvKzp6FLBUYW7tXYtsNbd/hk4o4pzCoGr66A2EXGlZe1j8MOD2VOcStEhsE0OQWQxVzUdw1tPvux1eeKD9A1VER+XfiCdyFlRHAjNgm0nEWSCCA2B69uNYsFfFnhdnvgohbuID8vMzqTHtChywrPgnVu55cwFPPYYhIZ6XZn4OoW7iI9KTM4iKi6Sgx0zOfHTW3g3fgEXXOB1VdJQKNxFfNDS5Vn893sR2B4Z9Nt+E9+sWkTz5l5XJQ2Jwl3Eh+zbB7fekc3bwZEQmc5lxWP54NUXvS5LGiCFu4jH0vancdWTV7MjNZM9KVASvgu6ZnN9yzG8Mellr8uTBkrhLuKh9APpRMRFkd0qC0IMhEAQhjGhN7D4rsVelycNmMJdxCPpWZl0nRpJ/klZBL97K4/9aQF33gnBwV5XJv5A4S7igW+/z2LwkxEUd9lPh28m8MXyBfTs6XVV4k8U7iLHUXExzHk0i5mbe0HPTM7OGMeXHy7U0gFS52q6nruI1NLGjXDG77OZuSkSemVwzQk38tX/PK9gl3qhnrtIPcrMzmTWkof55zcFfPstcMrb0GsfY0LG8FrsS16XJ35M4S5ST9IPpNPjgQhn6YCuOA8L17W8jtdidXckqV8Kd5F6sHtvJhEzIjnYPovmn4xl2pgbGDwY2oW1o1/3fl6XJwFA4S5SB7Lzsln86WJKSkv48UfLos0PUdptP31+msDXf19ISIjXFUqgUbiL1FJKRgrRD0WT0yrn18ZucGnxOP7+xkLP6pLApnAXqYW9mXvp/VBvckJzaPbJ9RxMGcSQIXDHjdGMOvdSr8uTAKZwFzlGafvTiJwVTU5YNiyPJarxE7ywDAYO9LoyEc1zFzkm+7LS6T4tipywA5iVdzB7zBOsX69gF9+hnrvIUfr3pgx+Ny+SQx2zaPevW/ls6VP07u11VSIVKdxFqlFaWkr+wXxKS+HphdlM+/cp0GM/v0+bwBerFmihL/FJCneRI0jJSKHfQ/3Y32r/r4094Jqm43hzgWbCiO9SuIscRtlMmOzQbMw/BxNcHEJUFIy54EKmXjMFrKXCwjCV90U8pHAXqULa/jQiZkaT29qZCTMq+gnmz4eTT3ZPiIuDrCyYN88JdGshNhbCwpxjIh7TbBmRSpJS0+kyNYrc1gc44aM7Wf7gE7z9drlgt9YJ9vh4J9DLgj0+3mm31sPqRRzquYuU8+HH6VyxJJLSzllE/nArX6+Kp3XrSicZ4/TYwQn0+HhnOybm1568iMeM9YFexqBBg2xCQoLXZUgAy82Fuydn8lxuBPTIZPjBCXz412oumFoLQeX++C0tVbDLcWWM2WCtHVTVMfXcJWClZKRw1pyzSD2UTlER2GZF0KOYP4WO45XYGgR7bGzFtthY9dzFZ2jMXQLS3sy9RD0Yza6WuziY2YKg/Ja0Km7NnR3v5JXY54/85PJj7DExTo89JqbiGLyIx9Rzl4CTtj+NntOjyQ/Pwbwdy/1XPsEDD0CzZjV8AWOcWTHlx9jLxuDDwtRzF5+gMXcJKJt+Suf0xyI41CGLtl/cyep58fTvf4wvpnnu4jGNuUvAsxaefjadmHWR2K5ZDE65lS9Xx9O4cS1etHKQK9jFh2jMXfzezp1w4SWZ3PmvKGy3/VzbdALrnltQu2AX8XHquYtfSslI4bqnrmd7chZ79oDtsgM65TCuzc08f4fWhBH/V224G2OaAV8ATd3zl1trZxpjugNLgTbABuAGa22RMaYpsBgYCGQA11prd9ZT/SK/sTdzL5GzepPXOhvCDYRDUKnhpvBxPH/7c16XJ3Jc1GRY5iBwobX2NKA/MMwYcyYwF5hnre0F7AfGueePA/a77fPc80SOi+R9aXSbFk1eq2yavR/L4r6llP61lJJHS3j+9mqmOIr4kWp77taZTpPr7jZ2Hxa4EPhvt/0VIA54BhjpbgMsB542xhjrC9NypOGrNCMlce9OJr16D/lF+WRnw9eZ/6S0Qza9vr+Trz58gnbtPKxVxEM1GnM3xgTjDL30AuYD24Esa22xe0oS0NHd7gjsBrDWFhtjDuAM3aRXes3xwHiALl261O5fIYGh0kqMiXt30uehCPJPcj+GzYHGMLzwNj5cEe9hoSLeq9FsGWttibW2P9AJOAOIru0vttYustYOstYOatu2bW1fTvxdpZUYd6Um0vfBCPLbFNNqxZ0wex9jdu5j98QcPpw73+tqRTx3VLNlrLVZxpjPgLOAMGNMI7f33glIdk9LBjoDScaYRkAozoVVkWNX7lugKc/E0zc3nrwOwLKphBXN5q2PDEOHeluiiC+ptudujGlrjAlzt08ALgI2A58BV7mnjQXedbffc/dxj3+q8XapE8aQ8sC99PpjMLkdgeX3cNew2WzcqGAXqawmwzLtgc+MMd8D64GPrbUfAPcBdxtjtuGMqb/gnv8C0MZtvxuYUvdlSyDa9NMeuk2JoqBTCa2Xj+XrzV8yj1haNFffQaSymsyW+R44vYr2n3HG3yu3FwJX10l1EtCycrNY8vkSSkstGzaU8HLiA9gueZzx1Ui+2PASTafE/nqjDC21K1KBvqEqPikxNZG+j/QlLyzPaTBAF7juP2ex5JMVWolRpBoKd/E5SfuSnGA/MY/Gq8ZQmnEql1wCE67vx4i44b8GeVnAK9hFfkPhLj4lJSOF6Id7kxeaB8um8vuT5/D8h9Cr12GeoGAXqZJWhRSfsTsthR7To8kLy6Xxu/fy7N1z+PTTIwS7iByWeu5yfFRzY4vPv97L0Bd7U9Ihh+4b7uaLvz9Kp04e1CniJxTuUv8qLRuAtZTeFUNxaAhF980gbk46j6f0gS7ZXJJ3Jx+9/7hGW0RqSeEu9av8sgEA8+aROHEcpxa+RHZr4G+zoQnQBca1up3nZ2lNGJG6oHCX+lV+ymJ8PLsWxtP3BsjrBHw1mCamBVFRcMOQYdw7+l5PSxXxJ7pBthwf1pJ0QhCRfwyioEMpLJvK+PPn8OijEBrqdXEiDZNukC3espYfb7mdU8c041CHQkLfuoWV/TowZKHVVEaReqKpkFK/rGXxFY/Tp/RVDnUqZODOu0m5MJQhb98BsbHOmLyI1Dn13KXe7NsHE27fx4q2D0PnHK5rHMOSxY87gd74kJYNEKlHCnepU4mpiZw791zSDmVy8CDQ/iCEFnNru4ksuO1J5yQtGyBS7xTuUmd2pe2iz5y+5IfmwY42NAo2nNi0OTd3u5FHb3q04skKdpF6pXCXOrErNYnIB/tysE0ejd6Zytw/zyEmBoKDva5MJDAp3OWYbE3ayoinRpBTnENxMaTZdGz4QTqvu5fP3p1Dz55eVygS2BTuctS2p2zntMdPo6BlAcEHm1JSApQEcUn2ZD5aNVcjLiI+QOEuR2XHnh2c8tgpFLQsoOOXD5H8+QOMGAELFkDHjl5XJyJlFO5SY4mpifR7tB8FLQswy+Io2v8AS5fCNdfo+qiIr9GXmKRGdqXtInp2X/JPzIdl0xlzxkx++AGuvVbBLuKL1HOXav2UmES/uX05FJ7HiR/dz9J5D3LppV5XJSJHonCX39ixZweTX5tMYXEh6enwTc5a7Mm59N82mc/XzCYkxOsKRaQ6CnepYHvKdueCaViB0xACnADXNrqHpW/M9bQ2Eak5hbv8ovxMmJbvziRv83hu/QvMmNaSduHqros0JAp3AZyZMH3n9qPgxAJYOosezWfw4lcwcKDXlYnIsdBsGX9VeSndIyytm5i6i6iH+lIQkk/Q8uk8fOMMEhIU7CINmXru/qiKG1ITG+sssRsXV+HUb75L4uyFfSk5KY8O/5zKJ+88SO/eHtQsInVK4e5vqrghNbGxzn5MDFk5+1n+z7cpKSll7eeWpZn3Qodchu6fzD9Wz9FCXyJ+QuHubyrdkPqXkI+JYfu9t3PKjI6/zoRpAZwAt4Tdw6IHNRNGxJ/oBtn+yloI+vWSyo7k7fR9zFk6IGjVWBoXRHDZpfDn0adz2WB9I0mkIdINsgNN2Ri7K7EZ9Hk4msI2h2DpLEaeMoP586F9ew9rFJF6pdky/qYs2N0x9q07dxLxp2YUhh+ixYrJLP/rdN55R8Eu4u8U7v7GGGdWTEwMKy6aRNTsfhxqV0i/NWPZdVlrRl+lVb5EAoGGZfxQ7j1x3HlPMi+90xs65nI1k1n21SNavlEkgCjc/cTWpK0MeHwAuWG5TkN7oBTubH8P8X/RTBiRQFNtuBtjOgOLgXaABRZZa+ONMa2BN4FuwE7gGmvtfmOMAeKBS4F84EZr7bf1U76AsybMqY+fRmHLAvhqMCc0bkZUFIw9/0ruuvIur8sTEQ/UpOdeDEyy1n5rjDkR2GCM+Ri4EVhjrX3EGDMFmALcBwwHItzHYOAZ96fUg8TURKLn9KMorACzbBZTRs9gxgxo1szrykTES9WGu7V2D7DH3c4xxmwGOgIjgSHuaa8Aa3HCfSSw2DoT6NcZY8KMMe3d15Fa2rRzE+c8eQ45jXOwQGlwKbSytFs7nVVvzqB/f68rFBFfcFRj7saYbsDpwDdAu3KBvRdn2Aac4N9d7mlJbluFcDfGjAfGA3Tp0uVo6w5Im3dtZuBTAznY8iAnpfUifZ+BUsPwTjfz3qf30khXUETEVeM4MMa0BN4G7rLWZptyMy+stdYYc1RfdbXWLgIWgfMN1aN5biDasnsLA54cwMHmB4lY/ze2rp7EuefC889DZKTX1YmIr6nRPHdjTGOcYH/dWvuO25xqjGnvHm8PpLntyUDnck/v5LbJMdqatJX+T/SnsHkhjZfPZc+/JjF/Pqxdq2AXkapVG+7u7JcXgM3W2ifKHXoPGOtujwXeLdf+J+M4Ezig8fZjt2PPDk7522kUtiyEpQ8ztPtkNm2C226rsHSMiEgFNRmWORu4AdhojPnObbsfeARYZowZByQC17jHPsSZBrkNZyrkTXVZcCDZlpRIn0f6cahVAc1WPsSimdP44x/1XSQRqV5NZst8BRwuToZWcb4FJtayroC0aecmrlpwFXnFeRQdgjRSsa2L6PP9TD795AHatav+NUREQN9Q9Rm/zIRpcZCgg00otUBJEFeXTmfZyjivyxORBkbh7gPKz4QJ/8ffSF8/iXHj4LHHoFUrr6sTkYZI4e6xXWm7nJkwLQphyVxalk5iycfwhz94XZmINGSab+Gxs+L+QGGIMxPmrhGT+c9/FOwiUnvquXskPR3+8JfZpJyylWYJZ/LZa9M480yvqxIRf6Ge+3FmLSxbBpGnJPHvLjMJzmzCzpc/UrCLSJ1SuB9HKSkwahRcey0UnH0JhJSwcPh82rUJ87o0EfEzGpY5DqyFC2NuY615DrpbzN1QGFLCwIMDuXnYzV6XJyJ+SOFez37+Gc6beBvJg58hKLU5nZp0oEkwhBeH8/5973tdnoj4KYV7PSkpgaeegnsX30HJiGdonhbGjjlbOalVuNeliUgAULjXg02bYNw4+CY3FkY/TUhWGFtnb1Gwi8hxowuqdaioCB58EE4/Hf59aBKMfpKQAyFsmbmZk1qd5HV5IhJA1HOvI+vXO731jRshetR9/NjvCU48cCJbZmzh5NYne12eiAQYhXst5efDkFvvY33+EoL7QJuzS/mxbTIts1vy4/QfFewi4gmFey2sXQsjp99K9tCFmLwggkoakWOgfU571t2/jg5tOnhdoogEKIX7MThwACZPhkVfT4RRC2mZ3oods38iPFQXTEXENyjcj8K0V6exbN1qEhPhEAUwahOhB8L46aEfFewi4lMU7jV07dyxLCtcDK2BMKetbXZbvp/xvWbCiIjPUbhXw1q4IPbPfB62GLaFM6X7VmbNDKNJE68rExE5PIX7ESQlwdm3jWfXgJdolNiGL2K2ctbvwrwuS0SkWvoSUxVKS+HZZ6HHlbeya8BzNN/bmpQnflKwi0iDoZ57Jdu2wS23wNoMZyZMSGYrts/dQnhoa69LExGpMYW765XVr/Hyio18+RWYVj/CqPcIOxDGllmaCSMiDY/CHRgRN5b3zWI4GbjKaQvdH8qWmVs0E0ZEGqSADveDB+HMv/yZ77ouxmwPJ6bv05x5piE4KIjLB19OsybNvC5RROSYBGy4r1sHw6fcQtaQl2iS1IYfHtpKz65hztxHY7wuT0SkVgJutkxeHsTGwlkTbiVryPO03N2cPY9v+TXYY2MhLs7rMkVEaiWgwn3NGjjlFHhyzUS4ciFhKU3Z8Wo+rWc+9Guwx8dDVpazLyLSQAXEsMzGrbuY8WAWK1dCSP9n4MKFhB0IY+ujPxHefLYT6PHxzskxMTBvnoZmRKRBM9YHeqiDBg2yCQkJ9fLaF0way9qWiyv8jRKyP4StM7c6M2GshaByB0tLFewi0iAYYzZYawdVdcxvh2VSU6H7f93E2pDFBO9qzSWHruf6E6/n5vCbKwZ7bGzFJ8bGakhGRBo8vxuWsRZeew1ufvoWioa/TPOUNux8YhttW4X99sSyMfayoZiyfdDQjIg0aH4V7rt2wYQJsCrlVhj1PKEZrfn5sZ9oHRL225ONgbCwimPs8+Y5x8LCFOwi0qD5xZh7aSksXAj33QeFERMpvmKBc8F01tbqlw6oPK9d89xFpIGo1Zi7MeZFY0yaMeY/5dpaG2M+NsZsdX+2ctuNMeYpY8w2Y8z3xpgBdffPqNqWLXD++TBxIrQ6J+aXYN8yc0vN1oSpHOQKdhHxAzW5oPoyMKxS2xRgjbU2Aljj7gMMByLcx3jgmbops2pn3f5Hop9pzFcDGxMU25jdZzxFyIEQrQkjIgGv2nC31n4BZFZqHgm84m6/AlxZrn2xdawDwowx7euo1t+I7tiL5hld6EIXugV1YWDxQDZP36xgF5GAd6wXVNtZa/e423uBdu52R2B3ufOS3LY9VGKMGY/Tu6dLly7HVMRLU+N4ibhjeq6IiD+r9Tx361yRPeqrstbaRdbaQdbaQW3btq1tGSIiUs6xhntq2XCL+zPNbU8GOpc7r5PbJiIix9Gxhvt7wFh3eyzwbrn2P7mzZs4EDpQbvhERkeOk2jF3Y8wSYAgQboxJAmYCjwDLjDHjgETgGvf0D4FLgW1APnBTPdQsIiLVqDbcrbXXH+bQ0CrOtcDE2hYlIiK147cLh4mIBDKFu4iIH1K4i4j4IZ9YOMwYsw/nwqyXwoF0j2s4Wqq5/jW0ekE1Hy++UHNXa22VXxTyiXD3BcaYhMOtruarVHP9a2j1gmo+Xny9Zg3LiIj4IYW7iIgfUrj/apHXBRwD1Vz/Glq9oJqPF5+uWWPuIiJ+SD13ERE/pHAXEfFDARvuxpidxpiNxpjvjDEJbluV94b1mjEmyq2z7JFtjLnLGBNnjEku136px3X69P12j6Lmx4wxP7p1rTDGhLnt3YwxBeXe74U+VPNhPwvGmKnu+7zFGHOJD9X8Zrl6dxpjvnPbPX+fjTGdjTGfGWN+MMZsMsbEuO0+/XmuwFobkA9gJxBeqe1RYIq7PQWY63WdVdQdjHP3q65AHHCP1zWVq+08YADwn+reU5zVQz8CDHAm8I0P1Xwx0Mjdnluu5m7lz/Ox97nKzwLQB/g30BToDmwHgn2h5krHHwdm+Mr7DLQHBrjbJwI/ue+lT3+eyz8Ctud+GIe7N6wvGQpst9Z6/Y3e37A+fL/dw6mqZmvtamttsbu7DuemMz7jMO/z4YwEllprD1prd+Asx31GvRV3GEeq2RhjcJYNX3JcizoCa+0ea+237nYOsBnnlqE+/XkuL5DD3QKrjTEb3Pu5wuHvDetLrqPifwS3u38Gvugrw0iVHO39dn3Nn3F6ZGW6G2P+zxjzuTHmXK+KOoyqPgsN4X0+F0i11m4t1+Yz77MxphtwOvANDejzHMjhfo61dgAwHJhojDmv/EHr/K3lU/NEjTFNgBHAW27TM0BPoD/OTcgf96aymvHF9/RIjDHTgGLgdbdpD9DFWns6cDfwhjEmxKv6KmlQn4VKrqdih8Vn3mdjTEvgbeAua212+WO+/nkO2HC31ia7P9OAFTh/qh7u3rC+YjjwrbU2FcBam2qtLbHWlgLP4cGf2zXQIO+3a4y5EbgcGOP+R4w7tJHhbm/AGb+O9KzIco7wWfD197kR8F/Am2VtvvI+G2Ma4wT769bad9zmBvN5DshwN8a0MMacWLaNcwHtPxz+3rC+okIPp9KY3iicf4OvaXD32zXGDAMmAyOstfnl2tsaY4Ld7R5ABPCzN1VWdITPwnvAdcaYpsaY7jg1/+/xru8I/gD8aK1NKmvwhffZvQ7wArDZWvtEuUMN5/Ps9RVdLx5AD5wZBP8GNgHT3PY2wBpgK/AJ0NrrWsvV3ALIAELLtb0KbAS+x/lwtfe4xiU4f1IfwhlzHHe49xRnVsF8nF7ZRmCQD9W8DWf89Dv3sdA9d7T7efkO+Ba4wodqPuxnAZjmvs9bgOG+UrPb/jLwl0rnev4+A+fgDLl8X+5zcKmvf57LP7T8gIiIHwrIYRkREX+ncBcR8UMKdxERP6RwFxHxQwp3ERE/pHAXEfFDCncRET/0/0i54EiWBaBIAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", + "display(fig)" + ] + }, + { + "cell_type": "markdown", + "id": "1692814f", + "metadata": {}, "source": [ "### Enjoy!" - ], - "metadata": {} + ] } ], "metadata": {}, diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index 81c777764..f4ddcad4a 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -2,84 +2,109 @@ "cells": [ { "cell_type": "markdown", + "id": "9ea014b3", + "metadata": {}, "source": [ "# Quantized Logistic Regression\n", "\n", "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "d341fd23", + "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our logistic regression model" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 1, + "id": "0a7429ff", + "metadata": {}, + "outputs": [], "source": [ "import numpy as np\n", "import torch" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "abfeea1b", + "metadata": {}, "source": [ "### And some helpers for visualization" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 2, + "id": "a3f970f3", + "metadata": {}, + "outputs": [], "source": [ + "%matplotlib inline\n", + "\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "c7a0cc5f", + "metadata": {}, "source": [ "### We need a dataset, a handcrafted one for simplicity" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 3, + "id": "809023a3", + "metadata": {}, + "outputs": [], "source": [ "x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n", "y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "2d522cb0", + "metadata": {}, "source": [ "### Let's visualize our dataset to get a grasp of it" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 4, + "id": "4cda7fe2", + "metadata": {}, + "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 5, + "id": "9b34b9a4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "x_min, x_max = x[:, 0].min(), x[:, 0].max()\n", "x_deviation = x_max - x_min\n", @@ -103,31 +128,22 @@ " color=\"blue\",\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "72076b9c", + "metadata": {}, "source": [ "### Now, we need a model so let's define it" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 6, + "id": "ae24e6a8", + "metadata": {}, + "outputs": [], "source": [ "class Model(torch.nn.Module):\n", " def __init__(self, n):\n", @@ -137,22 +153,47 @@ " def forward(self, x):\n", " output = torch.sigmoid(self.fc(x))\n", " return output" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "8d67d2a6", + "metadata": {}, "source": [ "### And create one\n", "\n", "The main purpose of this tutorial is not to train a logistic regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 7, + "id": "72440f47", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch: 1 | Loss: 0.568401038646698\n", + "Epoch: 101 | Loss: 0.13618899881839752\n", + "Epoch: 201 | Loss: 0.08024412393569946\n", + "Epoch: 301 | Loss: 0.05637403950095177\n", + "Epoch: 401 | Loss: 0.043313879519701004\n", + "Epoch: 501 | Loss: 0.035116538405418396\n", + "Epoch: 601 | Loss: 0.02950483374297619\n", + "Epoch: 701 | Loss: 0.025427138432860374\n", + "Epoch: 801 | Loss: 0.022332407534122467\n", + "Epoch: 901 | Loss: 0.01990474946796894\n", + "Epoch: 1001 | Loss: 0.01795022375881672\n", + "Epoch: 1101 | Loss: 0.016343189403414726\n", + "Epoch: 1201 | Loss: 0.014998838305473328\n", + "Epoch: 1301 | Loss: 0.013857820071280003\n", + "Epoch: 1401 | Loss: 0.012877327390015125\n", + "Epoch: 1501 | Loss: 0.012025881558656693\n" + ] + } + ], "source": [ "model = Model(x.shape[1])\n", "\n", @@ -171,64 +212,56 @@ "\n", " if e % 100 == 1 or e == epochs:\n", " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Epoch: 1 | Loss: 0.9475528597831726\n", - "Epoch: 101 | Loss: 0.13412582874298096\n", - "Epoch: 201 | Loss: 0.07946280390024185\n", - "Epoch: 301 | Loss: 0.05598355457186699\n", - "Epoch: 401 | Loss: 0.04308217763900757\n", - "Epoch: 501 | Loss: 0.034963589161634445\n", - "Epoch: 601 | Loss: 0.02939651347696781\n", - "Epoch: 701 | Loss: 0.025346478447318077\n", - "Epoch: 801 | Loss: 0.022270068526268005\n", - "Epoch: 901 | Loss: 0.019855139777064323\n", - "Epoch: 1001 | Loss: 0.01790979877114296\n", - "Epoch: 1101 | Loss: 0.016309652477502823\n", - "Epoch: 1201 | Loss: 0.014970536343753338\n", - "Epoch: 1301 | Loss: 0.013833633624017239\n", - "Epoch: 1401 | Loss: 0.012856445275247097\n", - "Epoch: 1501 | Loss: 0.012007634155452251\n" - ] - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b948f03b", + "metadata": {}, "source": [ "### Time to make some predictions" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 8, + "id": "086ad98b", + "metadata": {}, + "outputs": [], "source": [ - "contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 250)\n", - "contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 250)\n", + "contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 100)\n", + "contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 100)\n", "contour_plot_x_data, contour_plot_y_data = np.meshgrid(contour_plot_x_data, contour_plot_y_data)\n", "\n", "inputs = np.stack((contour_plot_x_data.flatten(), contour_plot_y_data.flatten()), axis=1)\n", "predictions = model(torch.tensor(inputs).float()).detach().numpy()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "e82c642f", + "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 9, + "id": "8b4b09d4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "contour = ax.contourf(\n", " contour_plot_x_data,\n", @@ -238,99 +271,97 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "fc8f7add", + "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 10, + "id": "6f961c04", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[4.53581047]\n", + " [2.3700912 ]]\n", + "-14.660101890563965\n" + ] + } + ], "source": [ "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", "b = model.fc.bias.flatten().tolist()[0]\n", "\n", "print(w)\n", "print(b)" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "[[4.53723335]\n", - " [2.37176466]]\n", - "-14.666179656982422\n" - ] - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "1e25c552", + "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "469c400f", + "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 11, + "id": "669f761a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from IPython.display import SVG\n", "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ], - "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": [ - "" - ], - "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" - }, - "metadata": {}, - "execution_count": 11 - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "fc365b14", + "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 12, + "id": "e95d696c", + "metadata": {}, + "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -484,60 +515,66 @@ " def apply(self, x):\n", " assert x.parameters == self.input_parameters\n", " return QuantizedArray(self.table[x.values], self.output_parameters)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "500a4168", + "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 13, + "id": "286dd2be", + "metadata": {}, + "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(w, parameter_bits)\n", "b_q = QuantizedArray.of(b, parameter_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b3914f63", + "metadata": {}, "source": [ "### And quantize our inputs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 14, + "id": "22ca3dd2", + "metadata": {}, + "outputs": [], "source": [ "input_bits = 5\n", "\n", "x = inputs\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "e8faf37f", + "metadata": {}, "source": [ "### Time to make quantized inference" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 15, + "id": "e882daa3", + "metadata": {}, + "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -548,20 +585,33 @@ "y_q = sigmoid.apply(intermediate_q)\n", "\n", "quantized_predictions = y_q.dequantize()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "4e94b1ce", + "metadata": {}, "source": [ "### And visualize the results" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 16, + "id": "8ad8b4f7", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -574,31 +624,22 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "80f6ce01", + "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 17, + "id": "db457e1f", + "metadata": {}, + "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (intermediate.max() - intermediate.min())\n", "zp_y = int(round(intermediate.min() * q_y))\n", @@ -614,12 +655,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "9c5bbd90", + "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -636,28 +677,32 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "568ef254", + "metadata": {}, "source": [ "### Let's import the concrete numpy package now!" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": null, - "source": [ - "import concrete.numpy as hnp" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 18, + "id": "668d59ba", + "metadata": {}, + "outputs": [], + "source": [ + "import concrete.numpy as hnp" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "6e8f3272", + "metadata": {}, + "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -682,20 +727,22 @@ "\n", "def infer(x_0, x_1):\n", " return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)]" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "45d86243", + "metadata": {}, "source": [ - "### Time to compile our quantized inference function" - ], - "metadata": {} + "### Let's compile our quantized inference function to it's operation graph for visualization" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, + "id": "a612859a", + "metadata": {}, + "outputs": [], "source": [ "dataset = []\n", "for x_i in x_q:\n", @@ -709,100 +756,159 @@ " },\n", " iter(dataset),\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "cf6d3de7", + "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ], - "metadata": {} - }, - { - "cell_type": "code", - "execution_count": 20, - "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "\n", - "%0 = Constant(2) # Integer\n", - "%1 = Constant(1) # Integer\n", - "%2 = x_0 # Integer\n", - "%3 = Constant(6) # Integer\n", - "%4 = x_1 # Integer\n", - "%5 = Constant(6) # Integer\n", - "%6 = Add(2, 3) # Integer\n", - "%7 = Add(4, 5) # Integer\n", - "%8 = Mul(6, 0) # Integer\n", - "%9 = Mul(7, 1) # Integer\n", - "%10 = Add(8, 9) # Integer\n", - "%11 = TLU(10) # Integer\n", - "return(%11)\n" - ] - } - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 21, - "source": [ - "hnp.draw_graph(homomorphic_model).show()" - ], + "id": "e79486c8", + "metadata": {}, "outputs": [ { - "output_type": "display_data", - "data": { - "text/plain": [ - "" - ], - "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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" - }, - "metadata": {} + "name": "stdout", + "output_type": "stream", + "text": [ + "%0 = Constant(2) # ClearScalar>\n", + "%1 = Constant(1) # ClearScalar>\n", + "%2 = x_0 # EncryptedScalar>\n", + "%3 = Constant(6) # ClearScalar>\n", + "%4 = x_1 # EncryptedScalar>\n", + "%5 = Constant(6) # ClearScalar>\n", + "%6 = Add(2, 3) # EncryptedScalar>\n", + "%7 = Add(4, 5) # EncryptedScalar>\n", + "%8 = Mul(6, 0) # EncryptedScalar>\n", + "%9 = Mul(7, 1) # EncryptedScalar>\n", + "%10 = Add(8, 9) # EncryptedScalar>\n", + "%11 = TLU(10) # EncryptedScalar>\n", + "return(%11)\n", + "\n" + ] } ], - "metadata": {} - }, - { - "cell_type": "markdown", "source": [ - "### Finally, it's time to make homomorphic inference\n", - "\n", - "Or, at least, simulate it until the compiler integration is complete." - ], - "metadata": {} + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ] }, { "cell_type": "code", "execution_count": 22, - "source": [ - "homomorphic_predictions = []\n", - "for x_0, x_1 in map(lambda x_i: (int(x_i[0]), int(x_i[1])), x_q):\n", - " evaluation = homomorphic_model.evaluate({0: x_0, 1: x_1})\n", - " inference = QuantizedArray(evaluation[homomorphic_model.output_nodes[0]], y_q.parameters)\n", - " homomorphic_predictions.append(inference.dequantize())\n", - "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" + "id": "8d937cec", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } ], - "outputs": [], - "metadata": {} + "source": [ + "hnp.draw_graph(homomorphic_model).show()" + ] }, { "cell_type": "markdown", + "id": "ed9eabc3", + "metadata": {}, "source": [ - "### And visualize it" - ], - "metadata": {} + "### It's time to compile the function to its homomorphic equivalent" + ] }, { "cell_type": "code", "execution_count": 23, + "id": "6df86e59", + "metadata": {}, + "outputs": [], + "source": [ + "engine = hnp.compile_numpy_function(\n", + " infer,\n", + " {\n", + " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", + " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", + " },\n", + " iter(dataset),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ea82278b", + "metadata": {}, + "source": [ + "### Finally, let's make homomorphic inference" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "790377e4", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d0e8ac9eb4174f29978c3d4e4b6f42c9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10000 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -815,27 +921,15 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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" - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "a3d368e7", + "metadata": {}, "source": [ "### Enjoy!" - ], - "metadata": {} + ] } ], "metadata": {}, diff --git a/script/nbmake_utils/notebook_test_timeout.py b/script/nbmake_utils/notebook_test_timeout.py deleted file mode 100644 index 862093404..000000000 --- a/script/nbmake_utils/notebook_test_timeout.py +++ /dev/null @@ -1,23 +0,0 @@ -import json -import sys - -from pathlib import Path - - -def main(): - path_to_glob = Path(sys.argv[1]) - notebooks = path_to_glob.glob("*.ipynb") - - for notebook_file in notebooks: - with open(notebook_file, "r") as f: - notebook_dict = json.load(f) - execution = notebook_dict["metadata"].get("execution", {}) - execution["timeout"] = 1000 - notebook_dict["metadata"]["execution"] = execution - - with open(notebook_file, "w", newline="\n") as f: - json.dump(notebook_dict, f, indent=1, ensure_ascii=False) - - -if __name__ == "__main__": - main() From 3d715b210fb5f6f3a5c496866954ec37cf5bd5ac Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 13 Sep 2021 14:43:14 +0200 Subject: [PATCH 0229/1104] release: increment the version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ecc14b968..5b6ea0a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.1.0rc1" +version = "0.1.0rc2" description = "Concrete Framework" authors = ["Zama "] packages = [ From b1cd997c774d97fe987361fa9437cff1009a0a45 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 13 Sep 2021 17:29:32 +0300 Subject: [PATCH 0230/1104] doc: select titles of user tutorials --- docs/index.rst | 6 +++--- docs/user/tutorial/ARITHMETIC_OPERATIONS.md | 3 +++ docs/user/tutorial/FIRST_TUTORIAL.md | 5 ----- docs/user/tutorial/SECOND_TUTORIAL.md | 3 --- docs/user/tutorial/{THIRD_TUTORIAL.md => TABLE_LOOKUP.md} | 2 +- docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md | 3 +++ 6 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 docs/user/tutorial/ARITHMETIC_OPERATIONS.md delete mode 100644 docs/user/tutorial/FIRST_TUTORIAL.md delete mode 100644 docs/user/tutorial/SECOND_TUTORIAL.md rename docs/user/tutorial/{THIRD_TUTORIAL.md => TABLE_LOOKUP.md} (51%) create mode 100644 docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md diff --git a/docs/index.rst b/docs/index.rst index db6e5e7ea..ee38d61d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,9 +13,9 @@ Concrete Framework's documentation :maxdepth: 2 :caption: Tutorial - user/tutorial/FIRST_TUTORIAL.md - user/tutorial/SECOND_TUTORIAL.md - user/tutorial/THIRD_TUTORIAL.md + user/tutorial/ARITHMETIC_OPERATIONS.md + user/tutorial/WORKING_WITH_FLOATING_POINTS.md + user/tutorial/TABLE_LOOKUP.md .. toctree:: :maxdepth: 2 diff --git a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md new file mode 100644 index 000000000..710b9fbf7 --- /dev/null +++ b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md @@ -0,0 +1,3 @@ +# Arithmetic Operations + +Umut to do: #312 diff --git a/docs/user/tutorial/FIRST_TUTORIAL.md b/docs/user/tutorial/FIRST_TUTORIAL.md deleted file mode 100644 index 4e747fe30..000000000 --- a/docs/user/tutorial/FIRST_TUTORIAL.md +++ /dev/null @@ -1,5 +0,0 @@ -# First Tutorial - -Umut to do: #312 - - diff --git a/docs/user/tutorial/SECOND_TUTORIAL.md b/docs/user/tutorial/SECOND_TUTORIAL.md deleted file mode 100644 index 26642fb74..000000000 --- a/docs/user/tutorial/SECOND_TUTORIAL.md +++ /dev/null @@ -1,3 +0,0 @@ -# Second Tutorial - -Umut to do: #313 diff --git a/docs/user/tutorial/THIRD_TUTORIAL.md b/docs/user/tutorial/TABLE_LOOKUP.md similarity index 51% rename from docs/user/tutorial/THIRD_TUTORIAL.md rename to docs/user/tutorial/TABLE_LOOKUP.md index 661b4e60f..1d92db930 100644 --- a/docs/user/tutorial/THIRD_TUTORIAL.md +++ b/docs/user/tutorial/TABLE_LOOKUP.md @@ -1,3 +1,3 @@ -# Third Tutorial +# Table Lookup Umut to do: #314 diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md new file mode 100644 index 000000000..4b396c886 --- /dev/null +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -0,0 +1,3 @@ +# Working With Floating Points + +Umut to do: #313 From 671646fc5e9f0f01bb4914ffbe09c4caf245665b Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 13 Sep 2021 17:29:35 +0300 Subject: [PATCH 0231/1104] doc: write compiling and executing document for users --- docs/user/howto/COMPILING_AND_EXECUTING.md | 64 +++++++++++++++++++++- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index 26b7a06dd..edd2c39c8 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -1,10 +1,68 @@ # Compiling and Executing -Umut or Arthur, who wants to do this part? +## Importing necessary components -## Compiling +Everything you need to compile and execute homomorphic functions is included in a single module. You can import it like so: -## Executing +```python +import concrete.numpy as hnp +``` +## Defining a function to compile +You need to have a python function that follows the [limits](../explanation/FHE_AND_FRAMEWORK_LIMITS.md) of the Concrete Framework. Here is a simple example: +```python +def f(x, y): + return x + y +``` + +## Compiling the function + +To compile the function, you need to provide what are the inputs that it's expecting. In the example function above, `x` and `y` could be scalars or tensors (though, for now, only dot between tensors are supported), they can be encrypted or clear, they can be signed or unsigned, they can have different bit-widths. So, we need to know what they are beforehand. We can do that like so: + +```python +x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) +y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) +``` + +In this configuration, both `x` and `y` are 3-bit unsigned integers, so they have the range of `[0, 2**3 - 1]` + +We also need a dataset. However, it's not the dataset used in traning as it doesn't contain any labels. It is to determine the bit-widths of the intermediate results so only the inputs are necessary. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. + +```python +dataset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] +``` + +Finally, we can compile our function to its homomorphic equivalent. + +```python +engine = hnp.compile_numpy_function( + f, {"x": x, "y": y}, + dataset=iter(dataset), +) +``` + +## Performing homomorphic evaluation + +You can use `.run(...)` method of `engine` returned by `hnp.compile_numpy_function(...)` to perform fully homomorphic evaluation. Here are some examples: + +```python +>>> engine.run(3, 4) +7 +>>> engine.run(1, 2) +3 +>>> engine.run(7, 7) +14 +>>> engine.run(0, 0) +0 +``` + +Be careful about the inputs, though. +If you were to run with values outside the range of the dataset, the result might not be correct. + +## Further reading + +- [Arithmetic Operations Tutorial](../tutorial/ARITHMETIC_OPERATIONS.md) +- [Working With Floating Points Tutorial](../tutorial/WORKING_WITH_FLOATING_POINTS.md) +- [Table Lookup Tutorial](../tutorial/TABLE_LOOKUP.md) From efaf72880cc12e26d7a15ca0316b945147552cce Mon Sep 17 00:00:00 2001 From: youben11 Date: Mon, 13 Sep 2021 15:27:24 +0100 Subject: [PATCH 0232/1104] docs(dev): mlir conversion --- docs/_static/mlir/MLIR_conversion.png | Bin 0 -> 22244 bytes docs/dev/explanation/COMPILATION.md | 4 ++-- docs/dev/explanation/MLIR.md | 27 +++++++++++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 docs/_static/mlir/MLIR_conversion.png diff --git a/docs/_static/mlir/MLIR_conversion.png b/docs/_static/mlir/MLIR_conversion.png new file mode 100644 index 0000000000000000000000000000000000000000..3f78d77f21f509a72b366f3168d824e92e5a7ba6 GIT binary patch literal 22244 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 literal 0 HcmV?d00001 diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index 5b9dc9321..b715e70d4 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -197,9 +197,9 @@ Assigned Data Types: - `3`: Clear\<**uint2**> - `+`: Encrypted\<**uint4**> -## MLIR Lowering +## MLIR Conversion -TODO: Ayoub +The actual compilation will be done by the concrete compiler, which is expecting an MLIR input. The MLIR conversion goes from an operation graph to its MLIR equivalent. You can read more about it [here](./MLIR.md) ## Example Walkthrough #1 diff --git a/docs/dev/explanation/MLIR.md b/docs/dev/explanation/MLIR.md index ad49e6ca3..4c8f520ce 100644 --- a/docs/dev/explanation/MLIR.md +++ b/docs/dev/explanation/MLIR.md @@ -1,4 +1,29 @@ # MLIR -To be done by Ayoub, #311 +MLIR is the intermediate representation used by the concrete compiler, so we need to convert the operation graph to MLIR, which will look something like the following, for a graph performing the dot between two input tensors. + +``` +func @main(%arg0: tensor<4xi7>, %arg1: tensor<4x!HLFHE.eint<6>>) -> !HLFHE.eint<6> { + %0 = "HLFHE.dot_eint_int"(%arg1, %arg0) : (tensor<4x!HLFHE.eint<6>>, tensor<4xi7>) -> !HLFHE.eint<6> + return %0 : !HLFHE.eint<6> +} +``` + +The different steps of the transformation are depicted in the figure below. We will explain each part separately later on. + +![MLIR Conversion](../../_static/mlir/MLIR_conversion.png) + +The conversion uses as input the operation graph to convert, as well as a dictionary of node converter functions. + +## Define function signature + +The first step would be to define the function signature (excluding return value at this point). We will convert input node's types to MLIR (e.g. convert `EncryptedTensor(Integer(64, is_signed=False), shape=(4,))` to `tensor<4xi64>`) and map their values to the argument of the function. So if we had an operation graph with one `EncryptedScalar(Integer(7, is_signed=False))`, we will get an MLIR function like `func @main(%arg0 : !HLFHE.eint<7>) -> ()`. Note that the return type would be detected automatically later on when returning MLIR values. + +## Convert nodes in the OpGraph + +After that, we will iterate over the operation graph, node by node, and fetch the appropriate conversion function for that node to do the conversion. Converters should be stored in a dictionary mapping a node to the converter function. All functions need to have the same signature `converter(node: IntermediateNode, preds: List[IntermediateNode], ir_to_mlir_node: dict, context: mlir.Context)`. +- The `node` will be just the node to convert, it will be used to get information about inputs and outputs. Each specific conversion might require a different set of information, so each function fetches those separately. +- `preds` would be the operands of the operation, as they are the input for the converted `node`. +- The `ir_to_mlir_node` is a mutable dict that we update as we traverse the graph. It maps nodes to their respective values in MLIR. We need this during the creation of an MLIR operation out of a node, the node's inputs will be operands for the operation, but we can't use them as is, we need their MLIR value. The first nodes to be added are the input nodes, which should map to the arguments of the MLIR function. Everytime we convert a node to its MLIR equivalent, we add the mapping between the node and the MLIR value, so that whenever this node will be used as input to another one, we can retrieve its MLIR value. This will also be useful to know which MLIR value(s) to return at the end, as we already can identify output node(s), it will be easy to retrieve their MLIR values using this data structure. +- The `context` should be loaded with the required dialects to be able to create MLIR operations and types for the compiler. From 8522e5828027d7cade0a826e3b4fab541db70d16 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 14 Sep 2021 17:01:30 +0200 Subject: [PATCH 0233/1104] refacto: rename 'dataset' into a clear 'inputset' closes #340 --- benchmarks/linear_regression.py | 6 ++-- benchmarks/logistic_regression.py | 6 ++-- .../common/bounds_measurement/__init__.py | 2 +- .../{dataset_eval.py => inputset_eval.py} | 24 +++++++-------- concrete/common/extensions/table.py | 2 +- concrete/numpy/compile.py | 30 +++++++++---------- docs/dev/explanation/COMPILATION.md | 10 +++---- docs/user/howto/COMPILING_AND_EXECUTING.md | 8 ++--- examples/QuantizedLinearRegression.ipynb | 12 ++++---- examples/QuantizedLogisticRegression.ipynb | 12 ++++---- ..._dataset_eval.py => test_inputset_eval.py} | 18 +++++------ tests/common/mlir/test_mlir_converter.py | 4 +-- tests/numpy/test_debugging.py | 2 +- 13 files changed, 68 insertions(+), 68 deletions(-) rename concrete/common/bounds_measurement/{dataset_eval.py => inputset_eval.py} (79%) rename tests/common/bounds_measurement/{test_dataset_eval.py => test_inputset_eval.py} (94%) diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index 34b51b38a..68f20444d 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -137,15 +137,15 @@ def main(): def function_to_compile(x_0): return table[(x_0 + zp_x) * w_0] - dataset = [] + inputset = [] for x_i in x_q: - dataset.append((int(x_i[0]),)) + inputset.append((int(x_i[0]),)) # Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits))}, - iter(dataset), + iter(inputset), ) # Measure: End diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index c6f1be799..899a874b1 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -203,9 +203,9 @@ def main(): def function_to_compile(x_0, x_1): return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)] - dataset = [] + inputset = [] for x_i in x_q: - dataset.append((int(x_i[0]), int(x_i[1]))) + inputset.append((int(x_i[0]), int(x_i[1]))) # Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( @@ -214,7 +214,7 @@ def main(): "x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), "x_1": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), }, - iter(dataset), + iter(inputset), ) # Measure: End diff --git a/concrete/common/bounds_measurement/__init__.py b/concrete/common/bounds_measurement/__init__.py index 9bd6c5c7a..a1ea8260d 100644 --- a/concrete/common/bounds_measurement/__init__.py +++ b/concrete/common/bounds_measurement/__init__.py @@ -1,2 +1,2 @@ """Bounds measurement module.""" -from . import dataset_eval +from . import inputset_eval diff --git a/concrete/common/bounds_measurement/dataset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py similarity index 79% rename from concrete/common/bounds_measurement/dataset_eval.py rename to concrete/common/bounds_measurement/inputset_eval.py index e8662f462..2103760af 100644 --- a/concrete/common/bounds_measurement/dataset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -1,4 +1,4 @@ -"""Code to evaluate the IR graph on datasets.""" +"""Code to evaluate the IR graph on inputsets.""" from typing import Any, Callable, Dict, Iterator, Tuple @@ -7,20 +7,20 @@ from ..operator_graph import OPGraph from ..representation.intermediate import IntermediateNode -def eval_op_graph_bounds_on_dataset( +def eval_op_graph_bounds_on_inputset( op_graph: OPGraph, - dataset: Iterator[Tuple[Any, ...]], + inputset: Iterator[Tuple[Any, ...]], min_func: Callable[[Any, Any], Any] = min, max_func: Callable[[Any, Any], Any] = max, ) -> Dict[IntermediateNode, Dict[str, Any]]: - """Evaluate the bounds with a dataset. + """Evaluate the bounds with a inputset. Evaluate the bounds for all output values of the operators in the graph op_graph over data - coming from the dataset + coming from the inputset Args: op_graph (OPGraph): The graph for which we want to determine the bounds - dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters min_func (Callable[[Any, Any], Any], optional): custom function to compute a scalar minimum @@ -35,11 +35,11 @@ def eval_op_graph_bounds_on_dataset( op_graph, stored with the node as key and a dict with keys "min" and "max" as value. """ - def check_dataset_input_len_is_valid(data_to_check): + def check_inputset_input_len_is_valid(data_to_check): custom_assert( len(data_to_check) == len(op_graph.input_nodes), ( - f"Got input data from dataset of len: {len(data_to_check)}, " + f"Got input data from inputset of len: {len(data_to_check)}, " f"function being evaluated has {len(op_graph.input_nodes)} inputs, please make " f"sure your data generator returns valid tuples of input values" ), @@ -48,8 +48,8 @@ def eval_op_graph_bounds_on_dataset( # TODO: do we want to check coherence between the input data type and the corresponding Input ir # node expected data type ? Not considering bit_width as they may not make sense at this stage - first_input_data = dict(enumerate(next(dataset))) - check_dataset_input_len_is_valid(first_input_data.values()) + first_input_data = dict(enumerate(next(inputset))) + check_inputset_input_len_is_valid(first_input_data.values()) first_output = op_graph.evaluate(first_input_data) # We evaluate the min and max func to be able to resolve the tensors min and max rather than @@ -59,9 +59,9 @@ def eval_op_graph_bounds_on_dataset( for node, value in first_output.items() } - for input_data in dataset: + for input_data in inputset: current_input_data = dict(enumerate(input_data)) - check_dataset_input_len_is_valid(current_input_data.values()) + check_inputset_input_len_is_valid(current_input_data.values()) current_output = op_graph.evaluate(current_input_data) for node, value in current_output.items(): node_bounds[node]["min"] = min_func(node_bounds[node]["min"], value) diff --git a/concrete/common/extensions/table.py b/concrete/common/extensions/table.py index 5326a8f6c..8fc3eac87 100644 --- a/concrete/common/extensions/table.py +++ b/concrete/common/extensions/table.py @@ -58,7 +58,7 @@ class LookupTable: if x < 0 or x >= len(table): raise ValueError( f"Lookup table with {len(table)} entries cannot be indexed with {x} " - f"(you should check your dataset)", + f"(you should check your inputset)", ) return table[x] diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 333e73d10..fd9cd6359 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -6,7 +6,7 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple import numpy from zamalang import CompilerEngine -from ..common.bounds_measurement.dataset_eval import eval_op_graph_bounds_on_dataset +from ..common.bounds_measurement.inputset_eval import eval_op_graph_bounds_on_inputset from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter @@ -54,7 +54,7 @@ def numpy_min_func(lhs: Any, rhs: Any) -> Any: def _compile_numpy_function_into_op_graph_internal( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - dataset: Iterator[Tuple[Any, ...]], + inputset: Iterator[Tuple[Any, ...]], compilation_configuration: CompilationConfiguration, compilation_artifacts: CompilationArtifacts, ) -> OPGraph: @@ -64,7 +64,7 @@ def _compile_numpy_function_into_op_graph_internal( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_artifacts (CompilationArtifacts): Artifacts object to fill @@ -105,10 +105,10 @@ def _compile_numpy_function_into_op_graph_internal( f"{', '.join(str(node) for node in offending_non_integer_nodes)}" ) - # Find bounds with the dataset - node_bounds = eval_op_graph_bounds_on_dataset( + # Find bounds with the inputset + node_bounds = eval_op_graph_bounds_on_inputset( op_graph, - dataset, + inputset, min_func=numpy_min_func, max_func=numpy_max_func, ) @@ -139,7 +139,7 @@ def _compile_numpy_function_into_op_graph_internal( def compile_numpy_function_into_op_graph( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - dataset: Iterator[Tuple[Any, ...]], + inputset: Iterator[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: @@ -149,7 +149,7 @@ def compile_numpy_function_into_op_graph( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use @@ -177,7 +177,7 @@ def compile_numpy_function_into_op_graph( return _compile_numpy_function_into_op_graph_internal( function_to_compile, function_parameters, - dataset, + inputset, compilation_configuration, compilation_artifacts, ) @@ -201,7 +201,7 @@ def compile_numpy_function_into_op_graph( def _compile_numpy_function_internal( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - dataset: Iterator[Tuple[Any, ...]], + inputset: Iterator[Tuple[Any, ...]], compilation_configuration: CompilationConfiguration, compilation_artifacts: CompilationArtifacts, show_mlir: bool, @@ -212,7 +212,7 @@ def _compile_numpy_function_internal( function_to_compile (Callable): The function you want to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_configuration (CompilationConfiguration): Configuration object to use @@ -230,7 +230,7 @@ def _compile_numpy_function_internal( op_graph = _compile_numpy_function_into_op_graph_internal( function_to_compile, function_parameters, - dataset, + inputset, compilation_configuration, compilation_artifacts, ) @@ -256,7 +256,7 @@ def _compile_numpy_function_internal( def compile_numpy_function( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - dataset: Iterator[Tuple[Any, ...]], + inputset: Iterator[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, show_mlir: bool = False, @@ -267,7 +267,7 @@ def compile_numpy_function( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - dataset (Iterator[Tuple[Any, ...]]): The dataset over which op_graph is evaluated. It + inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It needs to be an iterator on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use @@ -297,7 +297,7 @@ def compile_numpy_function( return _compile_numpy_function_internal( function_to_compile, function_parameters, - dataset, + inputset, compilation_configuration, compilation_artifacts, show_mlir, diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index b715e70d4..de333d6df 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -129,20 +129,20 @@ Let's take a closer look at the options we provide today. ### Dataset Evaluation -This is the simplest approach, but it requires a dataset to be provided by the user. +This is the simplest approach, but it requires an inputset to be provided by the user. -The dataset is not the dataset in the usual sense of ML as it doesn't require labels. +The inputset is not to be confused with the dataset which is classical in ML, as it doesn't require labels. Rather, it is a set of values which are typical inputs of the function. -The idea is to evaluate each input in the dataset and record the result of each operation in the operation graph. +The idea is to evaluate each input in the inputset and record the result of each operation in the operation graph. Then we compare the evaluation results with the current minimum/maximum values of each node and update the minimum/maximum accordingly. -After the entire dataset is evaluated, we assign a data type to each node using the minimum and the maximum value it contained. +After the entire inputset is evaluated, we assign a data type to each node using the minimum and the maximum value it contained. Here is an example, given this operation graph where `x` is encrypted: ![](../../_static/compilation-pipeline/two_x_plus_three.png) -and this dataset: +and this inputset: ``` [2, 3, 1] diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index edd2c39c8..834bb68e1 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -28,10 +28,10 @@ y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) In this configuration, both `x` and `y` are 3-bit unsigned integers, so they have the range of `[0, 2**3 - 1]` -We also need a dataset. However, it's not the dataset used in traning as it doesn't contain any labels. It is to determine the bit-widths of the intermediate results so only the inputs are necessary. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. +We also need an inputset. This latter is not to be confused with the dataset, which is used in training and contains labels. It is to determine the bit-widths of the intermediate results so only the inputs are necessary. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. ```python -dataset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] +inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] ``` Finally, we can compile our function to its homomorphic equivalent. @@ -39,7 +39,7 @@ Finally, we can compile our function to its homomorphic equivalent. ```python engine = hnp.compile_numpy_function( f, {"x": x, "y": y}, - dataset=iter(dataset), + inputset=iter(inputset), ) ``` @@ -59,7 +59,7 @@ You can use `.run(...)` method of `engine` returned by `hnp.compile_numpy_functi ``` Be careful about the inputs, though. -If you were to run with values outside the range of the dataset, the result might not be correct. +If you were to run with values outside the range of the inputset, the result might not be correct. ## Further reading diff --git a/examples/QuantizedLinearRegression.ipynb b/examples/QuantizedLinearRegression.ipynb index c6ef9331a..c38c6bac5 100644 --- a/examples/QuantizedLinearRegression.ipynb +++ b/examples/QuantizedLinearRegression.ipynb @@ -54,7 +54,7 @@ "id": "27f67e43", "metadata": {}, "source": [ - "### We need a dataset, a handcrafted one for simplicity" + "### We need an inputset, a handcrafted one for simplicity" ] }, { @@ -73,7 +73,7 @@ "id": "fba2eecb", "metadata": {}, "source": [ - "### Let's visualize our dataset to get a grasp of it" + "### Let's visualize our inputset to get a grasp of it" ] }, { @@ -640,14 +640,14 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = []\n", + "inputset = []\n", "for x_i in x_q:\n", - " dataset.append((int(x_i[0]),))\n", + " inputset.append((int(x_i[0]),))\n", "\n", "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", " infer,\n", " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", - " iter(dataset),\n", + " iter(inputset),\n", ")" ] }, @@ -723,7 +723,7 @@ "engine = hnp.compile_numpy_function(\n", " infer,\n", " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", - " iter(dataset),\n", + " iter(inputset),\n", ")" ] }, diff --git a/examples/QuantizedLogisticRegression.ipynb b/examples/QuantizedLogisticRegression.ipynb index f4ddcad4a..230a195a1 100644 --- a/examples/QuantizedLogisticRegression.ipynb +++ b/examples/QuantizedLogisticRegression.ipynb @@ -55,7 +55,7 @@ "id": "c7a0cc5f", "metadata": {}, "source": [ - "### We need a dataset, a handcrafted one for simplicity" + "### We need an inputset, a handcrafted one for simplicity" ] }, { @@ -74,7 +74,7 @@ "id": "2d522cb0", "metadata": {}, "source": [ - "### Let's visualize our dataset to get a grasp of it" + "### Let's visualize our inputset to get a grasp of it" ] }, { @@ -744,9 +744,9 @@ "metadata": {}, "outputs": [], "source": [ - "dataset = []\n", + "inputset = []\n", "for x_i in x_q:\n", - " dataset.append((int(x_i[0]), int(x_i[1])))\n", + " inputset.append((int(x_i[0]), int(x_i[1])))\n", " \n", "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", " infer,\n", @@ -754,7 +754,7 @@ " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " },\n", - " iter(dataset),\n", + " iter(inputset),\n", ")" ] }, @@ -839,7 +839,7 @@ " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " },\n", - " iter(dataset),\n", + " iter(inputset),\n", ")" ] }, diff --git a/tests/common/bounds_measurement/test_dataset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py similarity index 94% rename from tests/common/bounds_measurement/test_dataset_eval.py rename to tests/common/bounds_measurement/test_inputset_eval.py index e22687d95..8fa937c22 100644 --- a/tests/common/bounds_measurement/test_dataset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -1,11 +1,11 @@ -"""Test file for bounds evaluation with a dataset""" +"""Test file for bounds evaluation with a inputset""" from typing import Tuple import pytest -from concrete.common.bounds_measurement.dataset_eval import ( - eval_op_graph_bounds_on_dataset, +from concrete.common.bounds_measurement.inputset_eval import ( + eval_op_graph_bounds_on_inputset, ) from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer @@ -207,15 +207,15 @@ from concrete.numpy.tracing import trace_numpy_function ), ], ) -def test_eval_op_graph_bounds_on_dataset( +def test_eval_op_graph_bounds_on_inputset( function, input_ranges, expected_output_bounds, expected_output_data_type: Integer, ): - """Test function for eval_op_graph_bounds_on_dataset""" + """Test function for eval_op_graph_bounds_on_inputset""" - test_eval_op_graph_bounds_on_dataset_multiple_output( + test_eval_op_graph_bounds_on_inputset_multiple_output( function, input_ranges, (expected_output_bounds,), @@ -264,13 +264,13 @@ def test_eval_op_graph_bounds_on_dataset( ), ], ) -def test_eval_op_graph_bounds_on_dataset_multiple_output( +def test_eval_op_graph_bounds_on_inputset_multiple_output( function, input_ranges, expected_output_bounds, expected_output_data_type: Tuple[Integer], ): - """Test function for eval_op_graph_bounds_on_dataset""" + """Test function for eval_op_graph_bounds_on_inputset""" op_graph = trace_numpy_function( function, {"x": EncryptedScalar(Integer(64, True)), "y": EncryptedScalar(Integer(64, True))} @@ -281,7 +281,7 @@ def test_eval_op_graph_bounds_on_dataset_multiple_output( for y_gen in range_y: yield (x_gen, y_gen) - node_bounds = eval_op_graph_bounds_on_dataset( + node_bounds = eval_op_graph_bounds_on_inputset( op_graph, data_gen(*tuple(range(x[0], x[1] + 1) for x in input_ranges)) ) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index cca5d84db..f1378a0b8 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -204,8 +204,8 @@ def datagen(*args): ) def test_mlir_converter(func, args_dict, args_ranges): """Test the conversion to MLIR by calling the parser from the compiler""" - dataset = datagen(*args_ranges) - result_graph = compile_numpy_function_into_op_graph(func, args_dict, dataset) + inputset = datagen(*args_ranges) + result_graph = compile_numpy_function_into_op_graph(func, args_dict, inputset) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error diff --git a/tests/numpy/test_debugging.py b/tests/numpy/test_debugging.py index 3f32797ea..a7342c485 100644 --- a/tests/numpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -211,7 +211,7 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): # Remark that the bitwidths are not particularly correct (eg, a MUL of a 17b times 23b # returning 23b), since they are replaced later by the real bitwidths computed on the -# dataset +# inputset @pytest.mark.parametrize( "lambda_f,x_y,ref_graph_str", [ From 1666dfc3f1e561396ace141a9ac4ce7bef7bb162 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 14 Sep 2021 18:04:37 +0200 Subject: [PATCH 0234/1104] chore: pin github actions to sha1 - will set-up dependabot to regularly update these --- .github/workflows/continuous-integration.yaml | 22 +++++++++---------- .github/workflows/docker-env.yaml | 14 ++++++------ .github/workflows/package-watcher.yaml | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 3de54834e..d18eb6f58 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -34,15 +34,15 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@dc73133d4da04e56a135ae2246682783cc7c7cb6 with: python-version: ${{ matrix.python-version }} - name: Cache Installation Files - uses: actions/cache@v2 + uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 with: # Paths are Unix specific for now path: | @@ -73,7 +73,7 @@ jobs: make --keep-going pcc docs - name: Archive docs artifacts if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 with: name: html-docs path: docs/_build/html @@ -98,13 +98,13 @@ jobs: run: | ./script/actions_utils/coverage.sh ${{ github.base_ref }} - name: Archive test coverage - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} with: name: coverage path: coverage.html - name: Comment with coverage - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@82e7a0d3c51217201b3fedc4ddde6632e969a477 if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} with: path: diff-coverage.txt @@ -121,7 +121,7 @@ jobs: - name: Slack Notification if: ${{ always() }} continue-on-error: true - uses: rtCamp/action-slack-notify@v2 + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png @@ -139,14 +139,14 @@ jobs: steps: - name: Download Documentation id: download - uses: actions/download-artifact@v2 + uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60 with: name: html-docs - name: Publish Documentation to S3 id: publish if: ${{ steps.download.outcome == 'success' && !cancelled() }} - uses: jakejarvis/s3-sync-action@master + uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 with: args: --delete env: @@ -158,7 +158,7 @@ jobs: - name: Invalidate CloudFront Cache if: ${{ steps.publish.outcome == 'success' }} - uses: awact/cloudfront-action@master + uses: awact/cloudfront-action@8bcfabc7b4bbc0cb8e55e48527f0e3a6d681627c env: SOURCE_PATH: '/*' AWS_REGION: ${{ secrets.AWS_REGION }} @@ -169,7 +169,7 @@ jobs: - name: Slack Notification if: ${{ always() }} continue-on-error: true - uses: rtCamp/action-slack-notify@v2 + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml index f597bace2..9bf13f421 100644 --- a/.github/workflows/docker-env.yaml +++ b/.github/workflows/docker-env.yaml @@ -33,20 +33,20 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: registry: ghcr.io username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} - name: Build concretefhe-env Image if: ${{ success() && !cancelled() }} - uses: docker/build-push-action@v2 + uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 with: context: . builder: ${{ steps.buildx.outputs.name }} @@ -66,7 +66,7 @@ jobs: - name: Slack Notification if: ${{ always() }} continue-on-error: true - uses: rtCamp/action-slack-notify@v2 + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png @@ -96,7 +96,7 @@ jobs: exit 1 fi - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: registry: ghcr.io username: ${{ secrets.BOT_USERNAME }} @@ -116,7 +116,7 @@ jobs: - name: Slack Notification if: ${{ always() }} continue-on-error: true - uses: rtCamp/action-slack-notify@v2 + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index d0ca3444d..f01f7d4c7 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Compare image timestamps and notify run: | ./script/actions_utils/container_timestamp_check.sh \ From ced607a616131a4d114096a2719640607db24cfb Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 14 Sep 2021 18:15:17 +0200 Subject: [PATCH 0235/1104] chore: setup dependabot to update github actions pins --- .github/dependabot.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 000000000..4299d7522 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,9 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every sunday + interval: "weekly" + day: "sunday" From 3e89b878d29e3195cdce085eb0e9419d146bf0b4 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 9 Sep 2021 17:29:56 +0200 Subject: [PATCH 0236/1104] docs: fill information on quantization --- docs/user/explanation/QUANTIZATION.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/user/explanation/QUANTIZATION.md b/docs/user/explanation/QUANTIZATION.md index d93d53925..52b7df871 100644 --- a/docs/user/explanation/QUANTIZATION.md +++ b/docs/user/explanation/QUANTIZATION.md @@ -1,3 +1,25 @@ # Quantization -Arthur to do: #319 +From Wikipedia https://en.wikipedia.org/wiki/Quantization : + +> Quantization is the process of constraining an input from a continuous or otherwise large set of values (such as the real numbers) to a discrete set (such as the integers). + +## Why is it needed? + +Modern computing has long been using data types that use 32 or 64 bits (be that integers or floating point numbers), or even bigger data types. However due to the costly nature of FHE computations (see [the limits of FHE](FHE_AND_FRAMEWORK_LIMITS.md)), using such types with FHE is impractical (or plain impossible) to have computations executing in a reasonable amount of time. + +## The gist of quantization + +The basic idea of quantization is to take a range of values represented by a _large_ data type and represent it by a _smaller_ data type. This means some accuracy in the number's representation is lost, but in a lot of cases it is possible to adapt computations to still give meaningful results while using significantly less bits to sent the data used during those computations. + +## Quantization in practice + +To quantize a range of values on a smaller range of values, we first need to choose the data type that is going to be used. ConcreteLib, the library used in the Concrete Framework, is currently limited to 7 bits unsigned integers, so we'll use that for the example. Knowing that, for a value in the range `[min_range, max_range]`, we can compute the step of the quantization, which is `(max_range - min_range) / (2**n - 1)` where n is the number of bits, here 7, so in practice the quantization step is `step = (max_range - min_range) / 127`. This means the gap between consecutive representible values cannot be smaller than that `step` value which means there can be a substantial loss of precision. Every interval of length `step = (max_range - min_range) / 127` will be represented by a value in `[0..127]`. + +The IntelLabs distiller quantization documentation goes into a detailed explanation about the math to quantize values and how to keep computations consistent: [quantization algorithm documentation](https://intellabs.github.io/distiller/algo_quantization.html). + +## Resources + +- IntelLabs distiller explanation of quantization: [Distiller documentation](https://intellabs.github.io/distiller/algo_quantization.html) +- Lei Mao's blog on quantization: [Quantization for Neural Networks](https://leimao.github.io/article/Neural-Networks-Quantization/) +- Google paper on Neural Network quantization and integer only inference: [Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference](https://arxiv.org/abs/1712.05877) From 5e8a7c527b59b90114f6541ce71a831c3eeb0581 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 14 Sep 2021 18:22:24 +0200 Subject: [PATCH 0237/1104] fix: capitalization closes #356 --- docs/dev/explanation/COMPILATION.md | 38 +++++++++---------- .../explanation/TERMINOLOGY_AND_STRUCTURE.md | 2 +- .../explanation/FHE_AND_FRAMEWORK_LIMITS.md | 4 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index de333d6df..e564ac480 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -100,7 +100,7 @@ The implementation is a bit more complex than that but the idea is the same. Tracing is also responsible for indicating whether the values in the node would be encrypted or not, and the rule for that is if a node has an encrypted predecessor, it is encrypted as well. -## Topological Transforms +## Topological transforms The goal of topological transforms is to make more functions compilable. @@ -109,12 +109,12 @@ However, if the floating points operations are intermediate operations, they can Let's take a closer look at the transforms we perform today. -### Fusing Floating Point Operations +### Fusing floating point operations We decided to allocate a whole new chapter to explain float fusing. You can find it [here](./FLOAT-FUSING.md). -## Bounds Measurement +## Bounds measurement Given an operation graph, goal of the bound measurement step is to assign the minimal data type to each node in the graph. @@ -127,7 +127,7 @@ Bounds measurement is necessary because FHE supports limited precision, and we d There are several ways to perform bounds measurement. Let's take a closer look at the options we provide today. -### Dataset Evaluation +### Inputset evaluation This is the simplest approach, but it requires an inputset to be provided by the user. @@ -197,13 +197,13 @@ Assigned Data Types: - `3`: Clear\<**uint2**> - `+`: Encrypted\<**uint4**> -## MLIR Conversion +## MLIR conversion The actual compilation will be done by the concrete compiler, which is expecting an MLIR input. The MLIR conversion goes from an operation graph to its MLIR equivalent. You can read more about it [here](./MLIR.md) -## Example Walkthrough #1 +## Example walkthrough #1 -### Function to Homomorphize +### Function to homomorphize ``` def f(x): @@ -216,17 +216,17 @@ def f(x): x = EncryptedScalar(UnsignedInteger(2)) ``` -#### Corresponding Operation Graph +#### Corresponding operation graph ![](../../_static/compilation-pipeline/two_x_plus_three.png) -### Topological Transforms +### Topological transforms -#### Fusing Floating Point Operations +#### Fusing floating point operations This transform isn't applied since the computation doesn't involve any floating point operations. -### Bounds Measurement Using [2, 3, 1] as Dataset (same settings as above) +### Bounds measurement using [2, 3, 1] as inputset (same settings as above) Data Types: - `x`: Encrypted\<**uint2**> @@ -235,7 +235,7 @@ Data Types: - `3`: Clear\<**uint2**> - `+`: Encrypted\<**uint4**> -### MLIR Lowering +### MLIR lowering ``` module { @@ -250,9 +250,9 @@ module { ``` -## Example Walkthrough #2 +## Example walkthrough #2 -### Function to Homomorphize +### Function to homomorphize ``` def f(x, y): @@ -266,17 +266,17 @@ x = EncryptedScalar(UnsignedInteger(3)) y = EncryptedScalar(UnsignedInteger(1)) ``` -#### Corresponding Operation Graph +#### Corresponding operation graph ![](../../_static/compilation-pipeline/forty_two_minus_x_plus_y_times_two.png) -### Topological Transforms +### Topological transforms -#### Fusing Floating Point Operations +#### Fusing floating point operations This transform isn't applied since the computation doesn't involve any floating point operations. -### Bounds Measurement Using [(6, 0), (5, 1), (3, 0), (4, 1)] as Dataset +### Bounds measurement using [(6, 0), (5, 1), (3, 0), (4, 1)] as inputset Evaluation Result of `(6, 0)`: - `42`: 42 @@ -332,7 +332,7 @@ Data Types: - `*`: Encrypted\<**uint2**> - `+`: Encrypted\<**uint6**> -### MLIR Lowering +### MLIR lowering ``` module { diff --git a/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md index d273f2598..1d487fb52 100644 --- a/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md +++ b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md @@ -13,7 +13,7 @@ In this section we will go over some terms that we use throughout the project. - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node -## Module Structure +## Module structure In this section, we will discuss the module structure of `concretefhe` briefly. You are encouraged to check individual `.py` files to learn more! diff --git a/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md b/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md index 83b7f580e..fa4c9cfc7 100644 --- a/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md +++ b/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md @@ -1,6 +1,6 @@ # FHE and Concrete Framework Limits -## FHE Limits +## FHE limits FHE used to be an impossible thing to imagine, twenty years ago. Then, with advances due to [Craig Gentry](https://crypto.stanford.edu/craig/), this became a dream come true. And, even more recently, with several generations of new scheme, FHE became practical. @@ -16,7 +16,7 @@ In the scheme used in the Concrete Framework, namely [TFHE](https://tfhe.github. For most FHE scheme but TFHE, the application of a non-linear function is complicated and slow, if not impossible. Typically, this is a blocker, since activation functions _are_ non-linear. However, in the Concrete Framework, we use an operation called _programmable bootstrapping_ (described in this [white paper](https://whitepaper.zama.ai)), which allows to apply any table lookup: by quantizing the non-linear function, any function can thus be replaced. -## Concrete Framework Limits +## Concrete Framework limits Since this is an early version of the product, not everything is done, to say the least. What we wanted to tackle first was the cryptographic complexities. This is why we concentrated on the cryptographic part, and let some engineering problems for later. From bd8dca11d5677439753ec17518ceec11a92557ac Mon Sep 17 00:00:00 2001 From: youben11 Date: Tue, 14 Sep 2021 16:26:21 +0100 Subject: [PATCH 0238/1104] fix(TLU): extend TLU to 2 ** bit_width elements --- concrete/common/mlir/utils.py | 19 ++++++++++++++++ concrete/numpy/compile.py | 4 ++++ tests/common/mlir/test_mlir_converter.py | 29 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index c6dec7c8d..60779a3d7 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -10,6 +10,7 @@ from ..data_types.dtypes_helpers import ( value_is_scalar_integer, ) from ..operator_graph import OPGraph +from ..representation.intermediate import ArbitraryFunction def is_graph_values_compatible_with_mlir(op_graph: OPGraph) -> bool: @@ -63,3 +64,21 @@ def update_bit_width_for_mlir(op_graph: OPGraph): ): max_bit_width = max(max_bit_width, value_out.data_type.bit_width) _set_all_bit_width(op_graph, max_bit_width) + + +def extend_direct_lookup_tables(op_graph: OPGraph): + """Extend direct lookup tables to the maximum length the input bit width can support. + + Args: + op_graph: graph to update lookup tables for + """ + for node in op_graph.graph.nodes: + if isinstance(node, ArbitraryFunction) and node.op_name == "TLU": + table = node.op_kwargs["table"] + bit_width = cast(Integer, node.inputs[0].data_type).bit_width + expected_length = 2 ** bit_width + if len(table) > expected_length: + node.op_kwargs["table"] = table[:expected_length] + else: + repeat = expected_length // len(table) + node.op_kwargs["table"] = (table * repeat)[:expected_length] diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index fd9cd6359..ea3f964a9 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -11,6 +11,7 @@ from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( + extend_direct_lookup_tables, is_graph_values_compatible_with_mlir, update_bit_width_for_mlir, ) @@ -133,6 +134,9 @@ def _compile_numpy_function_into_op_graph_internal( # Update bit_width for MLIR update_bit_width_for_mlir(op_graph) + # TODO: workaround extend LUT #359 + extend_direct_lookup_tables(op_graph) + return op_graph diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index f1378a0b8..5769dffda 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -67,6 +67,20 @@ def lut(x): return table[x] +# TODO: remove workaround #359 +def lut_more_bits_than_table_length(x, y): + """Test lookup table when bit_width support longer LUT""" + table = LookupTable([3, 6, 0, 2, 1, 4, 5, 7]) + return table[x] + y + + +# TODO: remove workaround #359 +def lut_less_bits_than_table_length(x): + """Test lookup table when bit_width support smaller LUT""" + table = LookupTable([3, 6, 0, 2, 1, 4, 5, 7, 3, 6, 0, 2, 1, 4, 5, 7]) + return table[x] + + def dot(x, y): """Test dot""" return numpy.dot(x, y) @@ -184,6 +198,21 @@ def datagen(*args): }, (range(0, 8),), ), + ( + lut_more_bits_than_table_length, + { + "x": EncryptedScalar(Integer(64, is_signed=False)), + "y": EncryptedScalar(Integer(64, is_signed=False)), + }, + (range(0, 8), range(0, 16)), + ), + ( + lut_less_bits_than_table_length, + { + "x": EncryptedScalar(Integer(64, is_signed=False)), + }, + (range(0, 8),), + ), ( dot, { From c253219277dd6f37b1214093c9cee2b1d7f53355 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 10 Sep 2021 10:16:04 +0300 Subject: [PATCH 0239/1104] feat: implement performing and publishing benchmarks with a single make target --- .github/workflows/daily-benchmarks.yaml | 67 ++++++++++ .gitignore | 4 +- Makefile | 13 +- benchmarks/linear_regression.py | 3 + benchmarks/logistic_regression.py | 2 + poetry.lock | 121 ++++++++++++++---- pyproject.toml | 3 + ...enchmark_and_publish_findings_in_docker.sh | 29 +++++ .../extract_machine_info.py | 50 ++++++++ script/progress_tracker_utils/measure.py | 20 ++- 10 files changed, 273 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/daily-benchmarks.yaml create mode 100755 script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh create mode 100644 script/progress_tracker_utils/extract_machine_info.py diff --git a/.github/workflows/daily-benchmarks.yaml b/.github/workflows/daily-benchmarks.yaml new file mode 100644 index 000000000..cd71ff34b --- /dev/null +++ b/.github/workflows/daily-benchmarks.yaml @@ -0,0 +1,67 @@ +name: Daily Benchmarks +on: + workflow_dispatch: + schedule: + - cron: '0 22 * * *' # Everyday @ 22:00 + +jobs: + perform: + name: Run Benchmarks on EC2 and Publish Results to Progress Tracker + runs-on: ubuntu-20.04 + steps: + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-3 # Europe (Paris) + + - name: Start EC2 Instance + run: | + aws ec2 start-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} + + - name: Get Public IP Address of EC2 Instance + id: public-ip + run: echo "::set-output name=value::$(aws ec2 describe-instances --region eu-west-3 --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} --query 'Reservations[].Instances[].PublicIpAddress' --output text)" + + - name: Connect To EC2 Instance, Perform Benchmarks, Publish Results + uses: appleboy/ssh-action@1d1b21ca96111b1eb4c03c21c14ebb971d2200f6 + with: + host: ${{ steps.public-ip.outputs.value }} + username: ${{ secrets.BENCHMARKS_EC2_USERNAME }} + key: ${{ secrets.BENCHMARKS_EC2_SSH_KEY }} + script: | + cd ~/concretefhe-internal + make docker_publish_measurements + + - name: Copy Logs + uses: appleboy/scp-action@edc8ec9139a2687bcebf0249d0352ff2a988df00 + with: + host: ${{ steps.public-ip.outputs.value }} + username: ${{ secrets.BENCHMARKS_EC2_USERNAME }} + key: ${{ secrets.BENCHMARKS_EC2_SSH_KEY }} + source: "~/concretefhe-internal/logs/latest.log" + target: "~/latest.log" + + - name: Stop EC2 Instance + if: ${{ always() }} + run: | + aws ec2 stop-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} + + - name: Upload Logs + uses: actions/upload-artifact@v2 + with: + name: logs + path: ~/latest.log + + - name: Send Slack Notification + if: ${{ always() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: 'Publishing benchmarks finished with status ${{ job.status }} (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.gitignore b/.gitignore index 682d620d1..679f9b1ff 100644 --- a/.gitignore +++ b/.gitignore @@ -132,8 +132,8 @@ dmypy.json # Pyre type checker .pyre/ -# Benchmark results -.benchmarks.json +# Benchmark Artifacts +.benchmarks # concrete compilation artifacts .artifacts diff --git a/Makefile b/Makefile index 4789f9bb8..62e3704e4 100644 --- a/Makefile +++ b/Makefile @@ -131,6 +131,16 @@ docker_build_and_start: docker_build docker_start docker_bas: docker_build_and_start .PHONY: docker_bas +docker_publish_measurements: docker_build + git pull + mkdir -p .benchmarks + python script/progress_tracker_utils/extract_machine_info.py + docker run --rm -it \ + --volume /"$$(pwd)":/src \ + $(DEV_DOCKER_IMG) \ + /bin/bash -i ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh +.PHONY: docker_publish_measurements + docs: clean_docs @# Generate the auto summary of documentations poetry run sphinx-apidoc -o docs/_apidoc $(SRC_DIR) @@ -165,8 +175,7 @@ pytest_nb: .PHONY: pytest_nb benchmark: - poetry run python script/progress_tracker_utils/measure.py benchmarks \ - --output .benchmarks.json + poetry run python script/progress_tracker_utils/measure.py benchmarks .PHONY: benchmark jupyter: diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index 68f20444d..f5245804d 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -151,9 +151,12 @@ def main(): loss = 0 for x_i, y_i in zip(x_q, y): + x_i = [int(value) for value in x_i] + # Measure: Evaluation Time (ms) prediction = QuantizedArray(engine.run(*x_i), y_parameters).dequantize() # Measure: End + loss += (prediction - y_i) ** 2 # Measure: Loss = loss / len(y) diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index 899a874b1..125c00985 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -220,6 +220,8 @@ def main(): correct = 0 for x_i, y_i in zip(x_q, y): + x_i = [int(value) for value in x_i] + # Measure: Evaluation Time (ms) prediction = round(QuantizedArray(engine.run(*x_i), y_parameters).dequantize()) # Measure: End diff --git a/poetry.lock b/poetry.lock index bf25a86d8..580178e3e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -207,7 +207,7 @@ six = "*" [[package]] name = "decorator" -version = "5.0.9" +version = "5.1.0" description = "Decorators for Humans" category = "dev" optional = false @@ -223,21 +223,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.3.5" -description = "Run coverage and linting reports on diffs" +version = "6.2.1" +description = "Automatically find diff lines that need test coverage." category = "dev" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">= 3.6" [package.dependencies] chardet = ">=3.0.0" Jinja2 = ">=2.7.1" -jinja2_pluralize = ">=0.3.0,<0.4.0" -pluggy = ">=0.13.1,<0.14.0" -Pygments = ">=2.9.0,<3.0.0" - -[package.extras] -toml = ["tomli (>=1.2.1,<2.0.0)"] +jinja2-pluralize = "*" +pluggy = "*" +pygments = "*" [[package]] name = "docutils" @@ -270,7 +267,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-bugbear" -version = "21.4.3" +version = "21.9.1" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -813,7 +810,7 @@ python-versions = ">=3.5" [[package]] name = "networkx" -version = "2.6.2" +version = "2.6.3" description = "Python package for creating and manipulating graphs and networks" category = "main" optional = false @@ -951,14 +948,15 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" @@ -982,6 +980,17 @@ python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" +[[package]] +name = "psutil" +version = "5.8.0" +description = "Cross-platform lib for process and system monitoring in Python." +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -998,6 +1007,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "py-cpuinfo" +version = "8.0.0" +description = "Get CPU info with pure Python 2 & 3" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pycodestyle" version = "2.7.0" @@ -1147,6 +1164,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "0.19.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pytz" version = "2021.1" @@ -1278,7 +1306,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.1.2" +version = "4.2.0" description = "Python documentation generator" category = "dev" optional = false @@ -1541,7 +1569,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "8a3be3fe122eddfb9a28a4f789c4581311ada706406277b5e84beb431369163b" +content-hash = "b2650d98938713e30a9141050ab90466bd9ba8a1eb38eedffcaa5bbd0f150cc5" [metadata.files] alabaster = [ @@ -1723,16 +1751,16 @@ cycler = [ {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] decorator = [ - {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, - {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, + {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, + {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] defusedxml = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.3.5-py3-none-any.whl", hash = "sha256:d8f0949c8a57f7ed4b93d0012fbe9e77898571070cd35bd33e29d0c4b2045b0d"}, - {file = "diff_cover-6.3.5.tar.gz", hash = "sha256:53b90abb1d33ef0361d15c8761d260f572b981b82a104dfb70de67a461d8cd42"}, + {file = "diff_cover-6.2.1-py3-none-any.whl", hash = "sha256:cfe6333c9b83a28f91214e06e3a86108aeeb6a9a31233944ce8ef236121ba899"}, + {file = "diff_cover-6.2.1.tar.gz", hash = "sha256:b22fe97d118fadb2528bbc0a1f9fc370491b29b1b3fe2e2f1cdb94bf028814c7"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1747,8 +1775,8 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-21.4.3.tar.gz", hash = "sha256:2346c81f889955b39e4a368eb7d508de723d9de05716c287dc860a4073dc57e7"}, - {file = "flake8_bugbear-21.4.3-py36.py37.py38-none-any.whl", hash = "sha256:4f305dca96be62bf732a218fe6f1825472a621d3452c5b994d8f89dae21dbafa"}, + {file = "flake8-bugbear-21.9.1.tar.gz", hash = "sha256:2f60c8ce0dc53d51da119faab2d67dea978227f0f92ed3c44eb7d65fb2e06a96"}, + {file = "flake8_bugbear-21.9.1-py36.py37.py38-none-any.whl", hash = "sha256:45bfdccfb9f2d8aa140e33cac8f46f1e38215c13d5aa8650e7e188d84e2f94c6"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -2054,8 +2082,8 @@ nest-asyncio = [ {file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"}, ] networkx = [ - {file = "networkx-2.6.2-py3-none-any.whl", hash = "sha256:5fcb7004be69e8fbdf07dcb502efa5c77cadcaad6982164134eeb9721f826c2e"}, - {file = "networkx-2.6.2.tar.gz", hash = "sha256:2306f1950ce772c5a59a57f5486d59bb9cab98497c45fc49cbc45ac0dec119bb"}, + {file = "networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef"}, + {file = "networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51"}, ] notebook = [ {file = "notebook-6.4.3-py3-none-any.whl", hash = "sha256:b50eafa8208d5db966efd1caa4076b4dfc51815e02a805b32ecd717e9e6cc071"}, @@ -2179,8 +2207,8 @@ platformdirs = [ {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] prometheus-client = [ {file = "prometheus_client-0.11.0-py2.py3-none-any.whl", hash = "sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"}, @@ -2190,6 +2218,36 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, ] +psutil = [ + {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, + {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"}, + {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"}, + {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"}, + {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"}, + {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"}, + {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"}, + {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"}, + {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"}, + {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"}, + {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"}, + {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"}, + {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"}, + {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"}, + {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"}, + {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"}, + {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"}, + {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"}, + {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"}, + {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"}, + {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"}, + {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"}, + {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"}, + {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"}, + {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"}, + {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"}, + {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"}, + {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"}, +] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, @@ -2198,6 +2256,9 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +py-cpuinfo = [ + {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -2288,6 +2349,10 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] +python-dotenv = [ + {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, + {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, +] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, @@ -2449,8 +2514,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sphinx = [ - {file = "Sphinx-4.1.2-py3-none-any.whl", hash = "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544"}, - {file = "Sphinx-4.1.2.tar.gz", hash = "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13"}, + {file = "Sphinx-4.2.0-py3-none-any.whl", hash = "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"}, + {file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, diff --git a/pyproject.toml b/pyproject.toml index 5b6ea0a64..3ee790a56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ Sphinx = "^4.1.1" sphinx-rtd-theme = "^0.5.2" myst-parser = "^0.15.1" tqdm = "^4.62.2" +psutil = "^5.8.0" +py-cpuinfo = "^8.0.0" +python-dotenv = "^0.19.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh new file mode 100755 index 000000000..a2b287f0f --- /dev/null +++ b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Run benchmarks while logging the intermediate results +# Publish findings in the progress tracker + +initial_log=logs/$(date -u --iso-8601=seconds).log + +mkdir -p logs +make -s benchmark > "$initial_log" + +final_log=logs/$(date -u --iso-8601=seconds).log + +cat -s "$initial_log" | sed '1d; $d' > "$final_log" +rm "$initial_log" + +cp "$final_log" logs/latest.log + +if [ -f .env ] +then + # Set the last two environment variables in `.env` for the curl command below + # (https://gist.github.com/mihow/9c7f559807069a03e302605691f85572) + export $(cat .env | tail -n 2 | sed 's/#.*//g' | xargs -d '\n') +fi + +curl \ + -H 'Authorization: Bearer '"$PROGRESS_TRACKER_TOKEN"'' \ + -H 'Content-Type: application/json' \ + -d @.benchmarks/findings.json \ + -X POST "$PROGRESS_TRACKER_URL"/measurement diff --git a/script/progress_tracker_utils/extract_machine_info.py b/script/progress_tracker_utils/extract_machine_info.py new file mode 100644 index 000000000..0f81185a7 --- /dev/null +++ b/script/progress_tracker_utils/extract_machine_info.py @@ -0,0 +1,50 @@ +import cpuinfo +import dotenv +import json +import os +import platform +import psutil +import urllib.parse + + +def main(): + dotenv.load_dotenv() + + properties = [] + + cpu_value = cpuinfo.get_cpu_info()["brand_raw"].replace("(R)", "®").replace("(TM)", "™") + properties.append(["CPU", cpu_value]) + + vcpu_value = os.getenv("VCPU") + if vcpu_value is not None: + properties.append(["vCPU", vcpu_value]) + + ram_value = f"{psutil.virtual_memory().total / (1024 ** 3):.2f} GB" + properties.append(["RAM", ram_value]) + + os_value = os.getenv("OS_NAME") + if os_value is None: + os_value = f"{platform.system()} {platform.release()}" + properties.append(["OS", os_value]) + + name = os.getenv("MACHINE_NAME").strip() + if name is None: + name = platform.node().strip() + + id_ = name.lower() + id_ = id_.replace(" ", "-") + id_ = id_.replace("_", "-") + id_ = id_.replace(".", "-") + id_ = id_.replace("(", "") + id_ = id_.replace(")", "") + id_ = id_.replace("$/h", "-dollars-per-hour") + id_ = id_.strip() + id_ = urllib.parse.quote_plus(id_) + + machine = {"id": id_, "name": name, "properties": properties} + with open(".benchmarks/machine.json", "w") as f: + json.dump(machine, f, indent=2, ensure_ascii=False) + + +if __name__ == "__main__": + main() diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 5193f9a08..c8ff04c56 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -10,9 +10,9 @@ import tqdm def name_to_id(name): """Convert a human readable name to a url friendly id (e.g., `x + y` to `x-plus-y`)""" + name = name.replace("-", "minus") name = name.replace("**", "-to-the-power-of-") name = name.replace("+", "plus") - name = name.replace("-", "minus") name = name.replace("*", "times") name = name.replace("/", "over") name = name.replace("%", "percent") @@ -191,7 +191,12 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s working = False pbar.write(f" Failed (exited with {process.returncode})") - pbar.write(f"") + pbar.write(f" --------------------{'-' * len(str(process.returncode))}-") + + stderr = process.stderr.decode("utf-8") + for line in stderr.split("\n"): + if line.strip() != "": + pbar.write(f" {line}") pbar.update(samples) break @@ -235,17 +240,18 @@ def main(): parser = argparse.ArgumentParser(description="Measurement script for the progress tracker") parser.add_argument("base", type=str, help="directory which contains the benchmarks") - parser.add_argument("--output", type=str, help="file which the results will be saved to") parser.add_argument("--samples", type=int, default=30, help="number of samples to take") parser.add_argument("--keep", action='store_true', help="flag to keep measurement scripts") args = parser.parse_args() base = pathlib.Path(args.base) - output = pathlib.Path(args.output) samples = args.samples - result = {"metrics": {}, "targets": {}} + with open(".benchmarks/machine.json", "r") as f: + machine = json.load(f) + + result = {"machine": machine, "metrics": {}, "targets": {}} scripts = list(base.glob("*.py")) # Process each script under the base directory @@ -303,8 +309,8 @@ def main(): perform_measurements(script, script_without_extension, target_id, metrics, samples, result) # Dump the latest results to the output file - with open(output, "w") as f: - json.dump(result, f, indent=2) + with open(".benchmarks/findings.json", "w") as f: + json.dump(result, f, indent=2, ensure_ascii=False) # Delete the modified script if the user doesn't care if not args.keep: From ae3c179294e095390cebe2f10d843a3e238d5de3 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 15 Sep 2021 12:41:20 +0300 Subject: [PATCH 0240/1104] chore: disable attaching tty to docker during benchmarks --- Makefile | 6 ++---- .../benchmark_and_publish_findings_in_docker.sh | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 62e3704e4..5020e01f1 100644 --- a/Makefile +++ b/Makefile @@ -135,10 +135,8 @@ docker_publish_measurements: docker_build git pull mkdir -p .benchmarks python script/progress_tracker_utils/extract_machine_info.py - docker run --rm -it \ - --volume /"$$(pwd)":/src \ - $(DEV_DOCKER_IMG) \ - /bin/bash -i ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh + docker run --rm --volume /"$$(pwd)":/src $(DEV_DOCKER_IMG) \ + /bin/bash ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh .PHONY: docker_publish_measurements docs: clean_docs diff --git a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh index a2b287f0f..115d7f858 100755 --- a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh +++ b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -3,6 +3,14 @@ # Run benchmarks while logging the intermediate results # Publish findings in the progress tracker +source /src/.docker_venv/bin/activate +if [[ "$?" != "0" ]]; then + python3 -m venv /src/.docker_venv + source /src/.docker_venv/bin/activate + cd /src/ && make setup_env +fi +export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so + initial_log=logs/$(date -u --iso-8601=seconds).log mkdir -p logs From 7c00507baac37141f7d688b486809b9f28d03ac0 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 15 Sep 2021 13:18:07 +0300 Subject: [PATCH 0241/1104] chore: add waiting ssh to daily benchmarks action to avoid errors and fix copy logs step --- .github/workflows/daily-benchmarks.yaml | 28 ++++++++++++++++--------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/daily-benchmarks.yaml b/.github/workflows/daily-benchmarks.yaml index cd71ff34b..1899e9da6 100644 --- a/.github/workflows/daily-benchmarks.yaml +++ b/.github/workflows/daily-benchmarks.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -20,10 +20,19 @@ jobs: run: | aws ec2 start-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} + - name: Wait For The Instance To Get An IP Address + run: timeout 180 bash -c 'until [[ $(aws ec2 describe-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} --query 'Reservations[].Instances[].PublicIpAddress' --output text) != "" ]]; do sleep 0.1; done' + - name: Get Public IP Address of EC2 Instance id: public-ip run: echo "::set-output name=value::$(aws ec2 describe-instances --region eu-west-3 --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} --query 'Reservations[].Instances[].PublicIpAddress' --output text)" + - name: Hide Public IP Address From GitHub Logs + run: echo "::add-mask::${{ steps.public-ip.outputs.value }}" + + - name: Wait For The Instance To Accept SSH Connections + run: timeout 180 bash -c 'until nc -z ${{ steps.public-ip.outputs.value }} 22; do sleep 0.1; done' + - name: Connect To EC2 Instance, Perform Benchmarks, Publish Results uses: appleboy/ssh-action@1d1b21ca96111b1eb4c03c21c14ebb971d2200f6 with: @@ -34,14 +43,13 @@ jobs: cd ~/concretefhe-internal make docker_publish_measurements + - name: Write SSH Key To A File + run: echo "$SSH_KEY" > ~/ssh-key && chmod 400 ~/ssh-key + env: + SSH_KEY: ${{ secrets.BENCHMARKS_EC2_SSH_KEY }} + - name: Copy Logs - uses: appleboy/scp-action@edc8ec9139a2687bcebf0249d0352ff2a988df00 - with: - host: ${{ steps.public-ip.outputs.value }} - username: ${{ secrets.BENCHMARKS_EC2_USERNAME }} - key: ${{ secrets.BENCHMARKS_EC2_SSH_KEY }} - source: "~/concretefhe-internal/logs/latest.log" - target: "~/latest.log" + run: scp -o StrictHostKeyChecking=no -i ~/ssh-key ${{ secrets.BENCHMARKS_EC2_USERNAME }}@${{ steps.public-ip.outputs.value }}:~/concretefhe-internal/logs/latest.log ~/latest.log - name: Stop EC2 Instance if: ${{ always() }} @@ -49,7 +57,7 @@ jobs: aws ec2 stop-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} - name: Upload Logs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 with: name: logs path: ~/latest.log @@ -57,7 +65,7 @@ jobs: - name: Send Slack Notification if: ${{ always() }} continue-on-error: true - uses: rtCamp/action-slack-notify@v2 + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png From 4458c4bb7a1d4b71ebfa26f191f973038f9e264b Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 15 Sep 2021 13:49:13 +0200 Subject: [PATCH 0242/1104] doc: explain the importance of the representativity of the inputset closes #375 --- docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md index d8f037e97..3869b7368 100644 --- a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md +++ b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -17,6 +17,12 @@ For the latter kind of bugs, we encourage the user to have a look at: Once the user has tried to see if the bug was not her own, it is time to go further. +## Is the inputset sufficiently representative? + +A bug may happen if ever the inputset, which is internally used by the compilation core to set bit widths of some intermediate data, is not sufficiently representative. Notably, if ever, with all the inputs in the inputset, it appears that an intermediate data can be represented an `n`-bit integer, but for a particular computation, this same intermediate data needs a bit more bits to be represented, the FHE execution for this computation will result in a wrong output (as typically in integer overflows in classical programs). + +So, in general, when a bug appears, it may be a good idea to enlarge the inputset, and try to have random-looking inputs in this latter, following distribution of inputs used with the function. + ## Having a reproducible bug Once you're sure it is a bug, it would be nice to try to: From f2582600b32bcaf3dcd84d0426a3a3591d22c5ba Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 16:52:56 +0200 Subject: [PATCH 0243/1104] chore: set-up dependabot for poetry (using the pip ecosystem) --- .github/dependabot.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 4299d7522..1a2d4e9e6 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -7,3 +7,10 @@ updates: # Check for updates to GitHub Actions every sunday interval: "weekly" day: "sunday" + + - package-ecosystem: "pip" + directory: "/" + schedule: + # Check for updates to python dependencies every sunday + interval: "weekly" + day: "sunday" From 0fbe2fe00bacb26a0942da29b5b190d21675a11d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 12:47:02 +0200 Subject: [PATCH 0244/1104] build: build docker image if necessary before pipeline - remove workflow that won't be used anymore --- .github/workflows/continuous-integration.yaml | 151 ++++++++++++++++-- .github/workflows/docker-env.yaml | 127 --------------- 2 files changed, 138 insertions(+), 140 deletions(-) delete mode 100644 .github/workflows/docker-env.yaml diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index d18eb6f58..7b3277f1f 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -5,10 +5,13 @@ on: branches: - main + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + # Allows external webhook trigger repository_dispatch: types: - - env-docker-preflight + - rebuild-env-docker schedule: # * is a special character in YAML so you have to quote this string @@ -16,15 +19,96 @@ on: # Timezone is UTC, so Paris time is +2 during the summer and +1 during winter - cron: '0 22 * * 0' +env: + FORCE_REBUILD_DOCKER: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'repository_dispatch' && github.event.action == 'rebuild-env-docker') }} + ENV_DOCKERFILE: docker/Dockerfile.concretefhe-env + PREFLIGHT_IMAGE_BASE: ghcr.io/zama-ai/concretefhe-env:preflight + LATEST_IMAGE: ghcr.io/zama-ai/concretefhe-env:latest + BASE_IMAGE: ghcr.io/zama-ai/concretefhe-env + jobs: - build: + build_preflight_docker: concurrency: - group: ${{ github.ref }}-${{ github.event_name }} + group: ${{ github.ref }} + cancel-in-progress: true + + name: Build & Push the concretefhe-env preflight Docker Image + runs-on: ubuntu-20.04 + outputs: + image: ${{ steps.set_image.outputs.image || env.LATEST_IMAGE }} + needs-push: ${{ env.BUILD_DOCKER }} + force-rebuild-docker: ${{ env.FORCE_REBUILD_DOCKER }} + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - name: Get changed files + uses: Ana06/get-changed-files@a2f6df8c195e713211f9f6258baafc445149355b + id: files + with: + format: 'space-delimited' + - name: Should rebuild docker check + run : | + set +e + echo "${{ steps.files.outputs.all }}" | grep ${ENV_DOCKERFILE} + DOCKERFILE_CHANGED=$? + if [[ "${DOCKERFILE_CHANGED}" == "0" || "${FORCE_REBUILD_DOCKER}" == "true" ]]; then + echo "Should rebuild docker image!" + echo "BUILD_DOCKER=true" >> $GITHUB_ENV + else + echo "Docker image up to date." + echo "BUILD_DOCKER=false" >> $GITHUB_ENV + fi + - name: Set prefligh Docker image + id: set_image + if: ${{ fromJSON(env.BUILD_DOCKER) }} + run: | + PREFLIGHT_IMAGE_TAG=$(echo ${{ github.ref }} | sed -e 's/\//-/g') + PREFLIGHT_IMAGE="${PREFLIGHT_IMAGE_BASE}-${PREFLIGHT_IMAGE_TAG}" + echo "::set-output name=image::${PREFLIGHT_IMAGE}" + echo "PREFLIGHT_IMAGE=${PREFLIGHT_IMAGE}" >> $GITHUB_ENV + - name: Set up Docker Buildx + if: ${{ fromJSON(env.BUILD_DOCKER) }} + id: buildx + uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 + - name: Login to GitHub Container Registry + if: ${{ fromJSON(env.BUILD_DOCKER) }} + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + - name: Build concretefhe-env Image + if: ${{ success() && !cancelled() && fromJSON(env.BUILD_DOCKER) }} + uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 + with: + context: . + builder: ${{ steps.buildx.outputs.name }} + file: docker/Dockerfile.concretefhe-env + push: true + tags: "${{ env.PREFLIGHT_IMAGE }}" + no-cache: true + - name: Slack Notification + if: ${{ always() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Docker image preflight build ${{ env.PREFLIGHT_IMAGE }} finished with \ + status ${{ job.status }}. Rebuilt image: ${{ env.BUILD_DOCKER || 'false' }}." + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + build: + needs: [build_preflight_docker] + concurrency: + group: ${{ github.ref }} cancel-in-progress: true runs-on: ubuntu-20.04 container: - image: ${{ github.event.client_payload.image || 'ghcr.io/zama-ai/concretefhe-env' }} + image: ${{ needs.build_preflight_docker.outputs.image }} credentials: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} @@ -109,15 +193,6 @@ jobs: with: path: diff-coverage.txt recreate: true - - name: Trigger docker push workflow - if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.action == 'env-docker-preflight' }} - run: | - curl \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ - https://api.github.com/repos/${{ github.repository }}/dispatches \ - -d '{"event_type":"publish-env-docker","client_payload":{"preflight_status":"${{ job.status }}"}}' - name: Slack Notification if: ${{ always() }} continue-on-error: true @@ -132,6 +207,9 @@ jobs: publish-docs: needs: [build] + concurrency: + group: ${{ github.ref }} + cancel-in-progress: true runs-on: ubuntu-20.04 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} @@ -177,3 +255,50 @@ jobs: SLACK_MESSAGE: 'Publishing documentation finished with status ${{ job.status }}' SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + push-docker-image: + needs: [build_preflight_docker, build] + if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main' && fromJSON(needs.build_preflight_docker.outputs.needs-push)) || fromJSON(needs.build_preflight_docker.outputs.force-rebuild-docker) }} + + concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + + name: Push env docker image + runs-on: ubuntu-20.04 + env: + PREFLIGHT_IMAGE: ${{ needs.build_preflight_docker.outputs.image }} + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - name: Login to GitHub Container Registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + - name: Pull preflight image + run: | + docker pull ${PREFLIGHT_IMAGE} + - name: Retag to latest and epoch-sha1 and push + run: | + EPOCH=$(date +%s) + SHA1=$(git rev-parse HEAD) + TAGGED_IMAGE="${BASE_IMAGE}:${EPOCH}-${SHA1}" + docker tag ${PREFLIGHT_IMAGE} ${LATEST_IMAGE} + docker tag ${PREFLIGHT_IMAGE} ${TAGGED_IMAGE} + docker push ${LATEST_IMAGE} + docker push ${TAGGED_IMAGE} + + - name: Slack Notification + if: ${{ always() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Publishing docker image ${{ env.BASE_IMAGE }} finished with status \ + ${{ job.status }}" + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/docker-env.yaml b/.github/workflows/docker-env.yaml deleted file mode 100644 index 9bf13f421..000000000 --- a/.github/workflows/docker-env.yaml +++ /dev/null @@ -1,127 +0,0 @@ -name: Docker image (concretefhe dev/CI) - -on: - push: - branches: - - main - paths: - - docker/Dockerfile.concretefhe-env - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - - # Allows external webhook trigger - repository_dispatch: - types: - - rebuild-env-docker - - publish-env-docker - -env: - PREFLIGHT_IMAGE: ghcr.io/zama-ai/concretefhe-env:preflight - LATEST_IMAGE: ghcr.io/zama-ai/concretefhe-env:latest - BASE_IMAGE: ghcr.io/zama-ai/concretefhe-env - -jobs: - build_preflight_docker: - if: ${{ github.event_name != 'repository_dispatch' || github.event.action == 'rebuild-env-docker' }} - - concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - - name: Build & Push the concretefhe env Docker Image - runs-on: ubuntu-20.04 - - steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 - - name: Login to GitHub Container Registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ghcr.io - username: ${{ secrets.BOT_USERNAME }} - password: ${{ secrets.BOT_TOKEN }} - - name: Build concretefhe-env Image - if: ${{ success() && !cancelled() }} - uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - file: docker/Dockerfile.concretefhe-env - push: true - tags: "${{ env.PREFLIGHT_IMAGE }}" - no-cache: true - - name: Trigger CI pipeline with preflight image - if: ${{ success() && !cancelled() }} - run: | - curl \ - -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${{ secrets.BOT_TOKEN }}" \ - https://api.github.com/repos/${{ github.repository }}/dispatches \ - -d '{"event_type":"env-docker-preflight","client_payload":{"image":"${{ env.PREFLIGHT_IMAGE }}"}}' - - name: Slack Notification - if: ${{ always() }} - continue-on-error: true - uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 - env: - SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} - SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png - SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Docker image preflight build ${{ env.PREFLIGHT_IMAGE }} finished with \ - status ${{ job.status }}" - SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - - push-docker-image: - if: ${{ github.event_name == 'repository_dispatch' && github.event.action == 'publish-env-docker'}} - - concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - - name: Push env docker image - runs-on: ubuntu-20.04 - - steps: - - name: Check build went well with preflight image - env: - PREFLIGHT_STATUS: ${{ github.event.client_payload.preflight_status }} - run: | - if [[ "${PREFLIGHT_STATUS}" != "success" ]]; then - echo "Build with new image failed, aborting." - exit 1 - fi - - name: Login to GitHub Container Registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ghcr.io - username: ${{ secrets.BOT_USERNAME }} - password: ${{ secrets.BOT_TOKEN }} - - name: Pull preflight image - run: | - docker pull ${PREFLIGHT_IMAGE} - - name: Retag to latest and epoch and push - run: | - EPOCH=$(date +%s) - EPOCH_IMAGE="${BASE_IMAGE}:${EPOCH}" - docker tag ${PREFLIGHT_IMAGE} ${LATEST_IMAGE} - docker tag ${PREFLIGHT_IMAGE} ${EPOCH_IMAGE} - docker push ${LATEST_IMAGE} - docker push ${EPOCH_IMAGE} - - - name: Slack Notification - if: ${{ always() }} - continue-on-error: true - uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 - env: - SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} - SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png - SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Publishing docker image ${{ env.BASE_IMAGE }} finished with status \ - ${{ job.status }}" - SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From e382c899cf3f1818a2d0304718bdf0259079f1f6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 15:31:05 +0200 Subject: [PATCH 0245/1104] build: remove unused curl dependency in concretfhe-env docker image --- docker/Dockerfile.concretefhe-env | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index 39d81142f..86d3de0e9 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -2,7 +2,6 @@ FROM ghcr.io/zama-ai/zamalang-compiler RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ - curl \ python3.8 \ python3.8-tk \ python3.8-venv \ From a69975742ff4270b0e275a08889c45ab52262c7f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 14 Sep 2021 17:38:53 +0200 Subject: [PATCH 0246/1104] docs: move notebooks to docs and use nbsphinx to include them in the html - update env image to install pandoc required by nbsphinx --- Makefile | 2 +- docker/Dockerfile.concretefhe-env | 3 +- docs/conf.py | 8 ++- docs/index.rst | 7 +++ .../QuantizedLinearRegression.ipynb | 0 .../QuantizedLogisticRegression.ipynb | 0 .../figures/QuantizationVisualized.svg | 0 poetry.lock | 51 +++++++++++++------ pyproject.toml | 1 + 9 files changed, 54 insertions(+), 18 deletions(-) rename {examples => docs/user/advanced_examples}/QuantizedLinearRegression.ipynb (100%) rename {examples => docs/user/advanced_examples}/QuantizedLogisticRegression.ipynb (100%) rename {examples => docs/user/advanced_examples}/figures/QuantizationVisualized.svg (100%) diff --git a/Makefile b/Makefile index 5020e01f1..685896be7 100644 --- a/Makefile +++ b/Makefile @@ -169,7 +169,7 @@ strip_nb: .PHONY: strip_nb pytest_nb: - poetry run pytest --nbmake examples/*.ipynb + poetry run pytest --nbmake docs/user/advanced_examples/*.ipynb .PHONY: pytest_nb benchmark: diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index 86d3de0e9..5acd26e7c 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -7,7 +7,8 @@ RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ python3.8-venv \ python-is-python3 \ git \ - graphviz* && \ + graphviz* \ + pandoc && \ rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry diff --git a/docs/conf.py b/docs/conf.py index 803fd3289..1dc663ff0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,13 @@ release = "0.1" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.napoleon", "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.autosummary"] +extensions = [ + "myst_parser", + "nbsphinx", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", +] myst_enable_extensions = [ "amsmath", diff --git a/docs/index.rst b/docs/index.rst index ee38d61d9..a46b4c6ba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,6 +27,13 @@ Concrete Framework's documentation user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md user/howto/FAQ.md +.. toctree:: + :maxdepth: 2 + :caption: Advanced Examples + + user/advanced_examples/QuantizedLinearRegression.ipynb + user/advanced_examples/QuantizedLogisticRegression.ipynb + .. toctree:: :maxdepth: 2 :caption: Explanation diff --git a/examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb similarity index 100% rename from examples/QuantizedLinearRegression.ipynb rename to docs/user/advanced_examples/QuantizedLinearRegression.ipynb diff --git a/examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb similarity index 100% rename from examples/QuantizedLogisticRegression.ipynb rename to docs/user/advanced_examples/QuantizedLogisticRegression.ipynb diff --git a/examples/figures/QuantizationVisualized.svg b/docs/user/advanced_examples/figures/QuantizationVisualized.svg similarity index 100% rename from examples/figures/QuantizationVisualized.svg rename to docs/user/advanced_examples/figures/QuantizationVisualized.svg diff --git a/poetry.lock b/poetry.lock index 580178e3e..5d1987d45 100644 --- a/poetry.lock +++ b/poetry.lock @@ -155,7 +155,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.4" +version = "2.0.5" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -376,7 +376,7 @@ python-versions = "*" [[package]] name = "ipywidgets" -version = "7.6.4" +version = "7.6.5" description = "IPython HTML widgets for Jupyter" category = "dev" optional = false @@ -546,7 +546,7 @@ pygments = ">=2.4.1,<3" [[package]] name = "jupyterlab-widgets" -version = "1.0.1" +version = "1.0.2" description = "A JupyterLab extension." category = "dev" optional = false @@ -800,6 +800,22 @@ pydantic = ">=1.7.2,<2.0.0" Pygments = ">=2.7.3,<3.0.0" pytest = ">=6.1.2,<7.0.0" +[[package]] +name = "nbsphinx" +version = "0.8.7" +description = "Jupyter Notebook Tools for Sphinx" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +docutils = "*" +jinja2 = "*" +nbconvert = "!=5.4" +nbformat = "*" +sphinx = ">=1.8" +traitlets = "*" + [[package]] name = "nest-asyncio" version = "1.5.1" @@ -873,7 +889,7 @@ pyparsing = ">=2.0.2" [[package]] name = "pandocfilters" -version = "1.4.3" +version = "1.5.0" description = "Utilities for writing pandoc filters in python" category = "dev" optional = false @@ -1243,7 +1259,7 @@ test = ["flaky", "pytest", "pytest-qt"] [[package]] name = "qtpy" -version = "1.11.0" +version = "1.11.1" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." category = "dev" optional = false @@ -1569,7 +1585,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "b2650d98938713e30a9141050ab90466bd9ba8a1eb38eedffcaa5bbd0f150cc5" +content-hash = "ada0e822f539bf819ccb21e005b48d868fad05746132a871a85549e2b7f8a0be" [metadata.files] alabaster = [ @@ -1681,8 +1697,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, - {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, + {file = "charset-normalizer-2.0.5.tar.gz", hash = "sha256:7098e7e862f6370a2a8d1a6398cd359815c45d12626267652c3f13dec58e2367"}, + {file = "charset_normalizer-2.0.5-py3-none-any.whl", hash = "sha256:fa471a601dfea0f492e4f4fca035cd82155e65dc45c9b83bf4322dfab63755dd"}, ] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, @@ -1807,8 +1823,8 @@ ipython-genutils = [ {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] ipywidgets = [ - {file = "ipywidgets-7.6.4-py2.py3-none-any.whl", hash = "sha256:3ffd1baa741eb631e7a3a69d4df290de074ef697e0ef3176e33361b44cd91711"}, - {file = "ipywidgets-7.6.4.tar.gz", hash = "sha256:028bf014a0b1d77cb676fe163115f145aacdde0bb9a51c4166940e5b62a7d1d0"}, + {file = "ipywidgets-7.6.5-py2.py3-none-any.whl", hash = "sha256:d258f582f915c62ea91023299603be095de19afb5ee271698f88327b9fe9bf43"}, + {file = "ipywidgets-7.6.5.tar.gz", hash = "sha256:00974f7cb4d5f8d494c19810fedb9fa9b64bffd3cda7c2be23c133a1ad3c99c5"}, ] isort = [ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, @@ -1852,8 +1868,8 @@ jupyterlab-pygments = [ {file = "jupyterlab_pygments-0.1.2.tar.gz", hash = "sha256:cfcda0873626150932f438eccf0f8bf22bfa92345b814890ab360d666b254146"}, ] jupyterlab-widgets = [ - {file = "jupyterlab_widgets-1.0.1-py3-none-any.whl", hash = "sha256:841925a349bd9a9197c5506bd5461a321b09e6659a9b179a0096b561a92898c3"}, - {file = "jupyterlab_widgets-1.0.1.tar.gz", hash = "sha256:f94fb7fa1ddc8668e3f98d67a97cabe322e8d04b78b9eb988c7fde415d7a02df"}, + {file = "jupyterlab_widgets-1.0.2-py3-none-any.whl", hash = "sha256:f5d9efface8ec62941173ba1cffb2edd0ecddc801c11ae2931e30b50492eb8f7"}, + {file = "jupyterlab_widgets-1.0.2.tar.gz", hash = "sha256:7885092b2b96bf189c3a705cc3c412a4472ec5e8382d0b47219a66cccae73cfa"}, ] kiwisolver = [ {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, @@ -2077,6 +2093,10 @@ nbmake = [ {file = "nbmake-0.5-py3-none-any.whl", hash = "sha256:8a0b3ce9ca26320165c6de532c3d36445da1dd53c2c8fac4870ed900b3cbe538"}, {file = "nbmake-0.5.tar.gz", hash = "sha256:da9bf1bbc377c9d1d697f99952834017c39b4983e7e482a038dec705955a8ae9"}, ] +nbsphinx = [ + {file = "nbsphinx-0.8.7-py3-none-any.whl", hash = "sha256:8862f291f98c1a163bdb5bac8adf25c61585a81575ac5c613320c6f3fe5c472f"}, + {file = "nbsphinx-0.8.7.tar.gz", hash = "sha256:ff91b5b14ceb1a9d44193b5fc3dd3617e7b8ab59c788f7710049ce5faff2750c"}, +] nest-asyncio = [ {file = "nest_asyncio-1.5.1-py3-none-any.whl", hash = "sha256:76d6e972265063fe92a90b9cc4fb82616e07d586b346ed9d2c89a4187acea39c"}, {file = "nest_asyncio-1.5.1.tar.gz", hash = "sha256:afc5a1c515210a23c461932765691ad39e8eba6551c055ac8d5546e69250d0aa"}, @@ -2126,7 +2146,8 @@ packaging = [ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pandocfilters = [ - {file = "pandocfilters-1.4.3.tar.gz", hash = "sha256:bc63fbb50534b4b1f8ebe1860889289e8af94a23bff7445259592df25a3906eb"}, + {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"}, + {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"}, ] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, @@ -2451,8 +2472,8 @@ qtconsole = [ {file = "qtconsole-5.1.1.tar.gz", hash = "sha256:bbc34bca14f65535afcb401bc74b752bac955e5313001ba640383f7e5857dc49"}, ] qtpy = [ - {file = "QtPy-1.11.0-py2.py3-none-any.whl", hash = "sha256:bd8baebb80c4d0d97e4e5a5cf15695522f6acc1fecc20b94a70a01ddf6c9e27e"}, - {file = "QtPy-1.11.0.tar.gz", hash = "sha256:bbd61f8d6480a01cec39ad94249dbde7d0a8fce2aca61ff5037b645c4fd13e02"}, + {file = "QtPy-1.11.1-py2.py3-none-any.whl", hash = "sha256:78f48d7cee7848f92c49ab998f63ca932fddee4b1f89707d6b73eeb0a7110324"}, + {file = "QtPy-1.11.1.tar.gz", hash = "sha256:d471fcb9cf96315b564ad3b42ca830d0286d1049d6a44c578d3dc3836381bb91"}, ] regex = [ {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, diff --git a/pyproject.toml b/pyproject.toml index 3ee790a56..dc304f9ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ flake8-bugbear = "^21.4.3" Sphinx = "^4.1.1" sphinx-rtd-theme = "^0.5.2" myst-parser = "^0.15.1" +nbsphinx = "^0.8.7" tqdm = "^4.62.2" psutil = "^5.8.0" py-cpuinfo = "^8.0.0" From bc67b0cc1de3048398c8b1f1c363c72998e3671c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 17:48:47 +0200 Subject: [PATCH 0247/1104] chore: disable dependabot for pip dependencies - there is no way to group updates unfortunately... I'll set-up a custom workflow to update python deps as I would imagine later --- .github/dependabot.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 1a2d4e9e6..4299d7522 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -7,10 +7,3 @@ updates: # Check for updates to GitHub Actions every sunday interval: "weekly" day: "sunday" - - - package-ecosystem: "pip" - directory: "/" - schedule: - # Check for updates to python dependencies every sunday - interval: "weekly" - day: "sunday" From c54a05a9aa977a2bf84aeb9b2fcb1ec5d1a1cf41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Sep 2021 16:12:55 +0000 Subject: [PATCH 0248/1104] build(deps): bump Ana06/get-changed-files from 1.2 to 2.0.0 Bumps [Ana06/get-changed-files](https://github.com/Ana06/get-changed-files) from 1.2 to 2.0.0. - [Release notes](https://github.com/Ana06/get-changed-files/releases) - [Commits](https://github.com/Ana06/get-changed-files/compare/a2f6df8c195e713211f9f6258baafc445149355b...ea75ed777daf24d6e95f43957bd26b1eab20806c) --- updated-dependencies: - dependency-name: Ana06/get-changed-files dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 7b3277f1f..f295dd108 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Get changed files - uses: Ana06/get-changed-files@a2f6df8c195e713211f9f6258baafc445149355b + uses: Ana06/get-changed-files@ea75ed777daf24d6e95f43957bd26b1eab20806c id: files with: format: 'space-delimited' From 5871d4e1871eda1c3d477c866ab08f26ce2057cb Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 18:11:28 +0200 Subject: [PATCH 0249/1104] fix(tools): update Makefile for moved notebooks --- Makefile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 685896be7..c438c5af0 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ SHELL:=/bin/bash DEV_DOCKER_IMG:=concretefhe-dev DEV_DOCKERFILE:=docker/Dockerfile.concretefhe-dev SRC_DIR:=concrete +NOTEBOOKS_DIR:=docs/user/advanced_examples setup_env: poetry install @@ -27,8 +28,8 @@ check_python_format: .PHONY: check_python_format check_strip_nb: - poetry run python ./script/nbmake_utils/notebook_sanitize.py examples --check -.PHONY: strip_nb + poetry run python ./script/nbmake_utils/notebook_sanitize.py $(NOTEBOOKS_DIR) --check +.PHONY: check_strip_nb pylint: $(MAKE) --keep-going pylint_src pylint_tests pylint_benchmarks @@ -165,11 +166,11 @@ pydocstyle: .PHONY: pydocstyle strip_nb: - poetry run python ./script/nbmake_utils/notebook_sanitize.py examples + poetry run python ./script/nbmake_utils/notebook_sanitize.py $(NOTEBOOKS_DIR) .PHONY: strip_nb pytest_nb: - poetry run pytest --nbmake docs/user/advanced_examples/*.ipynb + poetry run pytest --nbmake $(NOTEBOOKS_DIR)/*.ipynb .PHONY: pytest_nb benchmark: From 2296bcd45736d959d4884dc7a29a4983f5717c08 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 16:56:18 +0200 Subject: [PATCH 0250/1104] refactor: do not require an iterator for inputset just iterable --- .../bounds_measurement/inputset_eval.py | 14 +++++----- concrete/numpy/compile.py | 26 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index 2103760af..8f83e02fc 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -1,6 +1,6 @@ """Code to evaluate the IR graph on inputsets.""" -from typing import Any, Callable, Dict, Iterator, Tuple +from typing import Any, Callable, Dict, Iterable, Tuple from ..debugging import custom_assert from ..operator_graph import OPGraph @@ -9,7 +9,7 @@ from ..representation.intermediate import IntermediateNode def eval_op_graph_bounds_on_inputset( op_graph: OPGraph, - inputset: Iterator[Tuple[Any, ...]], + inputset: Iterable[Tuple[Any, ...]], min_func: Callable[[Any, Any], Any] = min, max_func: Callable[[Any, Any], Any] = max, ) -> Dict[IntermediateNode, Dict[str, Any]]: @@ -20,8 +20,8 @@ def eval_op_graph_bounds_on_inputset( Args: op_graph (OPGraph): The graph for which we want to determine the bounds - inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterator on tuples which are of the same length than the number of + inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It + needs to be an iterable on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters min_func (Callable[[Any, Any], Any], optional): custom function to compute a scalar minimum between two values that can be encountered during evaluation (for e.g. numpy or torch @@ -48,7 +48,9 @@ def eval_op_graph_bounds_on_inputset( # TODO: do we want to check coherence between the input data type and the corresponding Input ir # node expected data type ? Not considering bit_width as they may not make sense at this stage - first_input_data = dict(enumerate(next(inputset))) + inputset_iterator = iter(inputset) + + first_input_data = dict(enumerate(next(inputset_iterator))) check_inputset_input_len_is_valid(first_input_data.values()) first_output = op_graph.evaluate(first_input_data) @@ -59,7 +61,7 @@ def eval_op_graph_bounds_on_inputset( for node, value in first_output.items() } - for input_data in inputset: + for input_data in inputset_iterator: current_input_data = dict(enumerate(input_data)) check_inputset_input_len_is_valid(current_input_data.values()) current_output = op_graph.evaluate(current_input_data) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index ea3f964a9..03a894464 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -1,7 +1,7 @@ """numpy compilation function.""" import traceback -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple import numpy from zamalang import CompilerEngine @@ -55,7 +55,7 @@ def numpy_min_func(lhs: Any, rhs: Any) -> Any: def _compile_numpy_function_into_op_graph_internal( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - inputset: Iterator[Tuple[Any, ...]], + inputset: Iterable[Tuple[Any, ...]], compilation_configuration: CompilationConfiguration, compilation_artifacts: CompilationArtifacts, ) -> OPGraph: @@ -65,8 +65,8 @@ def _compile_numpy_function_into_op_graph_internal( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterator on tuples which are of the same length than the number of + inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It + needs to be an iterable on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_artifacts (CompilationArtifacts): Artifacts object to fill during compilation @@ -143,7 +143,7 @@ def _compile_numpy_function_into_op_graph_internal( def compile_numpy_function_into_op_graph( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - inputset: Iterator[Tuple[Any, ...]], + inputset: Iterable[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: @@ -153,8 +153,8 @@ def compile_numpy_function_into_op_graph( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterator on tuples which are of the same length than the number of + inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It + needs to be an iterable on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use during compilation @@ -205,7 +205,7 @@ def compile_numpy_function_into_op_graph( def _compile_numpy_function_internal( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - inputset: Iterator[Tuple[Any, ...]], + inputset: Iterable[Tuple[Any, ...]], compilation_configuration: CompilationConfiguration, compilation_artifacts: CompilationArtifacts, show_mlir: bool, @@ -216,8 +216,8 @@ def _compile_numpy_function_internal( function_to_compile (Callable): The function you want to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterator on tuples which are of the same length than the number of + inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It + needs to be an iterable on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_configuration (CompilationConfiguration): Configuration object to use during compilation @@ -260,7 +260,7 @@ def _compile_numpy_function_internal( def compile_numpy_function( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - inputset: Iterator[Tuple[Any, ...]], + inputset: Iterable[Tuple[Any, ...]], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, show_mlir: bool = False, @@ -271,8 +271,8 @@ def compile_numpy_function( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - inputset (Iterator[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterator on tuples which are of the same length than the number of + inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It + needs to be an iterable on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use during compilation From 381c81b76c0f145273675aad604b53a0c2eea414 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 15 Sep 2021 18:08:17 +0200 Subject: [PATCH 0251/1104] refacto: remove iter usage for passing a dataset - it is still supported but not required and more confusing --- benchmarks/linear_regression.py | 2 +- benchmarks/logistic_regression.py | 2 +- benchmarks/single_table_lookup.py | 2 +- benchmarks/x_plus_42.py | 2 +- benchmarks/x_plus_y.py | 2 +- benchmarks/x_to_the_power_of_2.py | 2 +- docs/dev/explanation/COMPILATION.md | 2 +- .../QuantizedLinearRegression.ipynb | 379 ++++++------- .../QuantizedLogisticRegression.ipynb | 499 ++++++++---------- docs/user/howto/COMPILING_AND_EXECUTING.md | 2 +- tests/common/compilation/test_artifacts.py | 2 +- .../common/compilation/test_configuration.py | 4 +- tests/common/debugging/test_drawing.py | 2 +- tests/numpy/test_compile.py | 4 +- 14 files changed, 400 insertions(+), 506 deletions(-) diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index f5245804d..1f9f4327f 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -145,7 +145,7 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits))}, - iter(inputset), + inputset, ) # Measure: End diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index 125c00985..b91a91a4f 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -214,7 +214,7 @@ def main(): "x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), "x_1": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), }, - iter(inputset), + inputset, ) # Measure: End diff --git a/benchmarks/single_table_lookup.py b/benchmarks/single_table_lookup.py index 3a8fc23cf..d52523d67 100644 --- a/benchmarks/single_table_lookup.py +++ b/benchmarks/single_table_lookup.py @@ -20,7 +20,7 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - iter([(i,) for i in range(2 ** input_bits)]), + [(i,) for i in range(2 ** input_bits)], ) # Measure: End diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index aeb840a74..98a5c1c0a 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -15,7 +15,7 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - iter([(6,), (1,), (5,), (2,)]), + [(6,), (1,), (5,), (2,)], ) # Measure: End diff --git a/benchmarks/x_plus_y.py b/benchmarks/x_plus_y.py index d668259f4..829d1f9dc 100644 --- a/benchmarks/x_plus_y.py +++ b/benchmarks/x_plus_y.py @@ -16,7 +16,7 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, - iter([(6, 1), (1, 4), (5, 3), (2, 0), (7, 7)]), + [(6, 1), (1, 4), (5, 3), (2, 0), (7, 7)], ) # Measure: End diff --git a/benchmarks/x_to_the_power_of_2.py b/benchmarks/x_to_the_power_of_2.py index 33c8e84b4..5e003304a 100644 --- a/benchmarks/x_to_the_power_of_2.py +++ b/benchmarks/x_to_the_power_of_2.py @@ -15,7 +15,7 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - iter([(6,), (1,), (5,), (2,)]), + [(6,), (1,), (5,), (2,)], ) # Measure: End diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index e564ac480..664d3e57a 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -24,7 +24,7 @@ y = hnp.EncryptedScalar(hnp.UnsignedInteger(1)) # Compile the function to its homomorphic equivalent engine = hnp.compile_numpy_function( f, {"x": x, "y": y}, - iter([(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)]), + [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)], ) # Make homomorphic inference diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index c38c6bac5..1697a0d9f 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -2,129 +2,115 @@ "cells": [ { "cell_type": "markdown", - "id": "73e4f53d", - "metadata": {}, "source": [ "# Quantized Linear Regression\n", "\n", "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "15e6e686", - "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our linear regression model" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 1, - "id": "0c2101de", - "metadata": {}, - "outputs": [], "source": [ "import numpy as np" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "6f02c3d4", - "metadata": {}, "source": [ "### And some helpers for visualization" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 2, - "id": "91260335", - "metadata": {}, - "outputs": [], "source": [ "%matplotlib inline\n", "\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "27f67e43", - "metadata": {}, "source": [ "### We need an inputset, a handcrafted one for simplicity" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 3, - "id": "84b42c42", - "metadata": {}, - "outputs": [], "source": [ "x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", "y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "fba2eecb", - "metadata": {}, "source": [ "### Let's visualize our inputset to get a grasp of it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 4, - "id": "a8c83085", - "metadata": {}, - "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 5, - "id": "93e61f29", - "metadata": {}, + "source": [ + "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", + "display(fig)" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", - "display(fig)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "fd40fedf", - "metadata": {}, "source": [ "### Now, we need a model so let's define it\n", "\n", "The main purpose of this tutorial is not to train a linear regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 6, - "id": "4f95ae45", - "metadata": {}, - "outputs": [], "source": [ "class Model:\n", " w = None\n", @@ -146,160 +132,145 @@ "\n", " def evaluate(self, x):\n", " return x @ self.w + self.b" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "9b0c8d49", - "metadata": {}, "source": [ "### And create one" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 7, - "id": "ad97e3e0", - "metadata": {}, - "outputs": [], "source": [ "model = Model().fit(x, y)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "e18b52fd", - "metadata": {}, "source": [ "### Time to make some predictions" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 8, - "id": "5fd2e6bf", - "metadata": {}, - "outputs": [], "source": [ "inputs = np.linspace(40, 210, 100).reshape(-1, 1)\n", "predictions = model.evaluate(inputs)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "fd49b135", - "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 9, - "id": "e76b0343", - "metadata": {}, + "source": [ + "ax.plot(inputs, predictions, color=\"blue\")\n", + "display(fig)" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "ax.plot(inputs, predictions, color=\"blue\")\n", - "display(fig)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "0e080f5b", - "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 10, - "id": "32ebe574", - "metadata": {}, + "source": [ + "print(model.w)\n", + "print(model.b)" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "[[2.669915]]\n", "-3.2335143\n" ] } ], - "source": [ - "print(model.w)\n", - "print(model.b)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "b1b90d66", - "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "b3e45e1f", - "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 11, - "id": "7d878724", - "metadata": {}, + "source": [ + "from IPython.display import SVG\n", + "SVG(filename=\"figures/QuantizationVisualized.svg\")" + ], "outputs": [ { + "output_type": "execute_result", "data": { - "image/svg+xml": [ - "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" - ], + "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
", "text/plain": [ "" ] }, - "execution_count": 11, "metadata": {}, - "output_type": "execute_result" + "execution_count": 11 } ], - "source": [ - "from IPython.display import SVG\n", - "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "814afccd", - "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 12, - "id": "d81af434", - "metadata": {}, - "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -432,65 +403,59 @@ " domain = np.array(range(2**input_bits), dtype=np.uint)\n", " table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", " return QuantizedFunction(table)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "0ddd6342", - "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 13, - "id": "9189e38d", - "metadata": {}, - "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(model.w, parameter_bits)\n", "b_q = QuantizedArray.of(model.b, parameter_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "add5b6c2", - "metadata": {}, "source": [ "### And quantize our inputs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 14, - "id": "e1f94ff2", - "metadata": {}, - "outputs": [], "source": [ "input_bits = 6\n", "\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "37209a62", - "metadata": {}, "source": [ "### Time to make quantized inference" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 15, - "id": "131be184", - "metadata": {}, - "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -499,52 +464,48 @@ "y_q = x_q.affine(w_q, b_q, min_y, max_y, output_bits)\n", "\n", "quantized_predictions = y_q.dequantize()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "ea94c049", - "metadata": {}, "source": [ "### And visualize the results" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 16, - "id": "2ab0f580", - "metadata": {}, + "source": [ + "ax.plot(inputs, quantized_predictions, color=\"black\")\n", + "display(fig)" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "ax.plot(inputs, quantized_predictions, color=\"black\")\n", - "display(fig)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "ea85b0ea", - "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 17, - "id": "2d341f26", - "metadata": {}, - "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (max_y - min_y)\n", "zp_y = int(round(min_y * q_y))\n", @@ -560,12 +521,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "b6c4b6c0", - "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -582,32 +543,28 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "907fc5b1", - "metadata": {}, "source": [ "### Let's import the concrete numpy package now!" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 18, - "id": "15e7e265", - "metadata": {}, - "outputs": [], "source": [ "import concrete.numpy as hnp" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 19, - "id": "85034b43", - "metadata": {}, - "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -623,22 +580,20 @@ "\n", "def infer(x_0):\n", " return table[(x_0 + zp_x) * w_0]" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "91d4f22b", - "metadata": {}, "source": [ "### Let's compile our quantized inference function to it's operation graph for visualization" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 20, - "id": "d6bc9eee", - "metadata": {}, - "outputs": [], "source": [ "inputset = []\n", "for x_i in x_q:\n", @@ -647,27 +602,29 @@ "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", " infer,\n", " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", - " iter(inputset),\n", + " inputset,\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "2177fbd9", - "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 21, - "id": "e284fcc3", - "metadata": {}, + "source": [ + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "%0 = Constant(1) # ClearScalar>\n", "%1 = x_0 # EncryptedScalar>\n", @@ -680,112 +637,102 @@ ] } ], - "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 22, - "id": "bee209f2", - "metadata": {}, + "source": [ + "hnp.draw_graph(homomorphic_model).show()" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOMAAAGnCAYAAABFMOCCAABPy0lEQVR4nO2deXxURda/n84eSCCiAiHmJ4sECARBRBAMRGEcEQFlEREBcRlQcBmXEUVHR9xe35kRB15REBEcB0wQYYZFQZCEZUBBUFYJyL6phEBYsp/fH9Wd253ubKS7by/18OlP3773pu+5xf12VZ2qOsciIoJGozGbjBCzLdBoNAotRo3GR9Bi1Gh8hDAzL378OGzfrl4HDsDRo2rfyZOQmwulpZCXB8XFUKcOREZCVBTExUF8PCQkQJMmkJQEKSmQnAx165p5R/7FcWC79XUAOGrddxLIBUqBPKAYqANEAlFAHBAPJABNgCQgBUgGdPFfOhZvOXBKSmDrVsjMVK916+DUKfdew2JRwkxNhR49IC0NEhPdew1/pQTYCmRaX+sANxc/FpQwU4EeQBqgi7/aZHhUjIWFsHIlfPEFLFoEv/xS+fkhIdCoETRsCA0aQGgoxMZCWBhcuAAFBXDxIuTkqFr07Nmqbbj2Whg4EO66S9WewUQhsBL4AlgEVFH8hACNgIZAAyAUiEU1ny4ABcBFIAdVi1aj+LkWGAjchao9NRXiGTHu3QvTp8OsWfDbb87HQ0OVSDp1gnbtoG1baNUKGjdWwqsuFy7AoUOwcyfs2KGau+vXw5Ejrs/v2BHGjoV774WYmEu7N39gLzAdmAW4KH5CUSLpBLQD2gKtgMbUrN9yATgE7AR2oJq764EKip+OwFjgXiCAi/9Sca8YN2yASZNg2TIo/63NmsGdd0KvXnDTTVC/vruu6sy+fZCVpexYtgzOnXM8Xq8ejBkDzzyjauFAYQMwCVgGlP9PbQbcCfQCbgI8WPzsA7KsdiwDyhU/9YAxwDOoWlgDQAbiBjZtErntNhElQePVuLHIhAki33/vjqtcGhcviixaJHL33SIREY721a0r8swzIjk55tnnDjaJyG0iQrlXYxGZICImFr9cFJFFInK3iESIo311ReQZEfHz4ncX6bUS4+nTIuPGiYSGOj7k3buLpKeLFBa6yUw3ceKEyBtviMTHO9rbsKHIxx+LlJaabWHNOC0i40QkVBwf8u4iki4iPlb8ckJE3hCReHG0t6GIfCwiflb87ubSxbhkiUijRo4PdY8eIitXutM+z3Dhgsjkyc6iTEsTOXzYbOuqxxIRaSSOD3UPEfGD4pcLIjJZnEWZJiJ+UvyeoOZiLCwUefllkZAQ4yFu0kRk9mwPmOdhzp9X9xIZadxL/fqqVvdVCkXkZREJEeMhbiIiflj8cl7UvUSKcS/1RdXqQUjNxJiTI5Kaajy4FovIE0+InDvnIfO8xM6dIl26ON7Xa6+ZbZUzOSKSKsaDaxGRJ0TEz4tfdopIF3G8Lx8sfk9TfTEeOiTStq1jP2vpUk/a5l0KC0Wee86xxn/kEZHiYrMtUxwSkbbi2M8KoOKXQhF5Thxr/EdExEeK3xtUT4zHj4u0aGE8pCkp/tO3qilffCESHW3c68MPm+/YOS4iLcR4SFMkcPtWX4hItBj3+rAEjWOnajHm5op06GA8nD17Ki9qILN2rUiDBsY9T5xoni25ItJBjIezpygvaiCzVkQaiHHPJha/N6lajAMGGA9l167+3z+sLhs2qHFI273Pm2eOHQPEeCi7iv/3D6vLBlHjkLZ7N6n4vUl6pUuopk1Tc0oBWreGxYuDZ1VEly4wf74xPW/MGLWyxJtMQ80pBWgNLCZ4VkV0AeZjTM8bg1pZEshUKMYDB+Dpp9V2VBSkp8Pll3vJKh/httvgpZfU9pkz8OCD3rv2AcBa/EQB6UCQFT+3Adbi5wzgxeI3hQrFOHGiWiEB8NZbvrviYevWrfTt25e4uDhiY2Pp3bs369atc9v3T5wI3bur7VWrVOvAG0xErZAAeAvPrnhYunQpSUlJhFUxS/+mm27CYrG4fD355JMesW0iYC1+VqFaB4GKSzFu2QJz56rtdu1g/HhvmlR9Nm7cSLdu3YiNjWXXrl3s37+f5s2bk5aWxvLly91yjdBQmDJFLe8CmDDBeRK8u9kCWIufdoCnin/fvn3079+f559/npMnT3roKrUjFJiC8aBOwHkSfMDgqif58MOG42LxYm/3Y6tHSUmJtG3bVuLj4+XChQtl+4uLi6VVq1aSmJgo+fn5brve8OFGmXzzjdu+1iUPi+G48GTxDxs2TN58800pKiqShIQECQ0NrfT87t27y3fffedBiypmuBhl8o0pFngcZwfOxYuQkaG2mzeH22/39s9D9cjKymLHjh0MHjyY6Ojosv2hoaEMGzaMw4cPs9iNbcrHHze2P/rIbV/rxEXAWvw0BzxZ/DNnzmTChAlVNk99Abvix4PFbypOYlyxQsWfAeWwsFi8bFE1WbVqFQDXX3+90zHbvpUrV7rtejfcYPSbFy5UYUQ8wQpU/BlQDgtPFr/9j5ivcwNGv3khKoxIoOEkxrVrjW131YrlO/733XcfAL1793bYn2v7FagGu3fvBuCqq65yOpaQkADAnj17am+8HbbyyMuDH39061eXYVf8Hq0VL5VPPvmEDh06ULduXerXr09qair/+te/vHJtW3nkAR4qflNxEuPGjeo9JsZ9HtS1a9eydetW6taty7XXXssHH3wAwJIlS+jSpQtz585FRIiLi6v2d9qEW9fFwGeMNabG6dOna227Pd26GdsbNrj1q8uwFj8x+GbMmNOnT/PRRx/xyy+/8O2339KsWTOGDx/O4/bteA9hV/x4qPhNxUmMtvgxSUnKk+gurr32WmbNmsUPP/zAyJEjERHGjBlDr169uOeee9x3IUCs7k6Lm9vYycnGdkVxdmqL7WuTUJ5EX2Lt2rXMmTOH6667jrp169KqVSvmzJnDDTfcwJQpU9ho+yX3EHbFX2GcHX/GSYy2AFJXXun+iw0ZMoSJEyeyYMECbrrpJk6dOsWkSZMu6btstej58+edjtn21aSmrQ72kx5cBdpyB7av9UDxe4zBgwcD8J///Mej17Gf9OCh4jcVl95UAE/17SdNmkSXLl1Yv349Q4YMISTk0oKat27dGoAjLqqoo0ePApCUlHTphrrAvkXs4jfALdgG+v3HtQLx8fEA/FJVLM5aYt8h8VDxm4qTEi67TL27ubtVxurVqzlz5gwpKSk8+uij/PDDD5f0PTfffDMAmzdvdjpm29erV69LN9QF9kGXPTU10Fr8eKj4PcKxY8cAaOjhUHv2QZcDcWqgkxhtD5knJmTs37+fBx98kM8//5x///vfREdHM2DAAH799dcaf1fPnj1JTk5m/vz55Ofnl+0vKSlh3rx5JCYm0rdvX3ea7xCEuUEDt351GbaHzNfmw3z44Yd06tTJab+IkJ6eDkC/fv08aoN9veuh4jcVJzG2aqXe9+xRk6Pdxblz57jzzjuZPHkyycnJNG3alPnz53Ps2DEGDx5MUVFRjb4vJCSEmTNnkpOTw+jRozlx4gSnTp1i3LhxZGdnM2PGDKKiotx3A8B33xnbbdq49avLsBY/e1CTo32J77//nnHjxrF3717y8/P56aefGDFiBJs3b+axxx6jS5cuHr2+XfHjoeI3l/Jzcv73f41pX1995Z55PuPGjRPUlEIBZNu2bfLrr7867ANk0qRJNf7u77//Xvr06SP16tWTmJgYueWWW2Tt2rXuMbwco0cbZXPokEcuIf8rxrQvNxV/hfznP/9x+j+wvWbMmOFwbn5+vmRkZMhdd90lLVq0kMjISKlfv76kpaXJv/71Lw9bqhgtRtl4qPjNJN0povi336q1fKBm4Hz4oZd+FXyc/HyV9SonR0VH//lnz1znW9RaPlAzcHTxK/JRWa9yUNHRPVT8ZuKcubhzZzXGCPDZZ86h8YOVhQuVEAFGjPDcdTqjxhgBPsM5NH6wshAlRAAPFr+pOInRYoH771fb587Bu+962SIfpLQU3n5bbVssMGqU565lAe63bp8DdPGrPJHW4scCeLD4TcXlIN+YMcYQx9tvV53KzZ1UtHjV/vXKK694zyBgzhy1xhNg6FC1msWTjMEY4nibqlO5BTpzUGs8AYaiVrMEIhVmofrrX+HZZ9X2wIHw+efeNMt3OHlSpa87eRIiIlT6uRYtPH/dvwLW4mcgEKTFz0lU+rqTQAQq/ZwXit8MnPuMNsaPV0GoABYsgBkzvGWT71BaCsOHG2Ouf/yjd4QIanW/tfhZAARh8VMKDMcYc/0jAStERWW+1h9/FImKUq78iAj3DXX4C08+aQxldOokUlDg3ev/KCJRolz5EeL5oQ5f40kxhjI6iYiXi9/bVB6qMSXFcFwUFsKQIeBi9llA8uqrMHmy2r7sMpg3TzVTvUkKhuOiEBgCBEnx8yow2bp9GTAP1UwNaKoj2aefNmqImBiRL7/09I+EeZSWqsxUtvuNjhbJyjLXpqfFqCFiRCSAi19KRWWmst1vtIiYXPzeonq5NkpLRUaNMh7QyEj/TAFXFefPiwwbZtxnRITKemw2pSIySowHNFL8MwVcVZwXkWFi3GeEqKzHQUL1s1CVrzFAZMQIkbw8D5rnRXbuVAl97FsAy5aZbZVB+RoDERkhIgFS/LJTVEIf+xaADxW/N6h5stR33hEJCzMe2pYtRZYv94BpXiI/X+T11x0zTyUmimzZYrZlrnlHRMLEeGhbiogfF7/ki8jr4ph5KlFEtphok0lcWhrxdetErr7asZYcMkRk/373WudpliwRSUpyvI8BA0ROnTLbsspZJyJXi2MtOURE9ptn0iWxRESSxPE+BoiIjxe/p7g0MYqoLMYjR6osv7YHOTxc5KGHRH7+2Z02up9ly1RGLXsRxsWJTJtmfi7G6pIjIiNFZfm1PcjhIvKQiPh48csyURm17EUYJyLTJGhyMbri0sVoIyvLsa8FKvtv794i6em+k/n3zBmRDz5wzDVpX6ufOGG2hZdGljj2tRCV/be3iKSL72T+PSMiH4hjrkn7Wt1Pi9+d1F6MIiJFRSIffyxyzTXOD3rTpiLPPqvyHXq71jl3TiQjQ+SeexxzLYKq0fv1E9m0ybs2eYIiEflYRK4R5we9qYg8KyrfobdrnXMikiEi94hjrkVE1ej9RCQAit9dOK9nrA3FxSphztSpal1keRIS4JZboEcPSE01ogq4i4ICtRo/MxPWrIGsLCPAlo3ISBg0CJ56ClxEkfBrilEJc6ai1kWWJwG4BegBpGJEFXAXBajV+JnAGiALI8CWjUhgEPAUEGDFX1sy3CpGe7ZsgenTVcLRisIa1qunYpGmpKg1lPHxkJgIjRqpY1FRKiJbRIRazlVUBGfPqtfhw2rO6MGDavL29u2Qna1+EFzRrh2MHAmjR8MVV3jijn2LLcB0VMLRisIa1kPFIk1BraGMBxKBRtZjUaiIbBGo5VxFwFnr6zBqzuhB1OTt7UA26gfBFe2AkcBoIAiK/1LwnBhtlJSommrBAvjqK9i715NXOwPUByA8XC2U7tdPrTpxc9RGv6EEVVMtAL4CHIr/zBmoX98j1w1HLZTuh1p1EqTFXxM8L8byHDumxLl+varNtm1zDIF4KYSEQEzMeCIj9/Doo8tJTYWuXYMn5XlNOIYS58pffuHj5s2pu3AhZ3v3rtV3hgBNUTVsB1QTuCvBk/LcTXhfjK44cULFlDl+HI4eVc3PvDzVB7xwQb3HxkJYmMoBUq+e6n/Gx6v3li1h9eol9OvXj507d5YFONZUzKRJk3jnnXc4cuQIZ+vU4WfgOHAU1fzMQ/UBL1jfY4EwVA6Qeqj+Z7z1vSVaeG7AN8ToDkSE1q1bc+uttzJlyhSzzfFpiouLyxLWvPXWW2abo1FUvLjY37BYLPzhD39g9uzZnD171mxzfJoFCxZw7NgxxowZY7YpGjsCRowADz74IKWlpcyZM8dsU3yaqVOn0q9fP5o1a2a2KRo7AkqMcXFxDB8+nH/84x8ESOvb7Wzfvp01a9Ywfvx4s03RlCOgxAjw+OOPs3fvXr7++muzTfFJ/vGPf9CmTRu3JwXS1J6AE2Pbtm3p0aMHU6dONdsUnyM3N5d//etfjBs3zu2JZDW1J+DECDB+/HgWL17M/v37zTbFp/jwww8JCQlhhCdDomsumYAU45133kmTJk2YNm2a2ab4DKWlpUybNo3Ro0dTr149s83RuCAgxRgWFsaYMWP48MMPuXDhgtnm+ARLlixh//79PPLII2aboqmAgBQjwJgxY7h48SJz58412xSfYOrUqfzud7/Ts5N8mIAV45VXXsmQIUP0bBwgOzubr7/+Wg9n+DgBK0aAJ554gh9++IG1a9eabYqpTJ06lcTERG6//XazTdFUQkCLsVOnTtxwww1BPcxx7tw5Zs+ezfjx4wkNDTXbHE0lBLQYQQ1zfP755xw9etRsU0xh9uzZFBYWMnr0aLNN0VRBwItx6NChXH755UyfPt1sU0zh/fff57777uPyyy832xRNFQS8GCMiInjooYd4//33KSgoMNscr/L111+zfft2xo4da7YpmmoQ8GIEePTRRzl9+jSfB1nG16lTp5Kamsp1111ntimaahAUYmzSpAkDBgwIKkfOoUOHWLx4sR7O8COCQoygHDn//e9/2bRpk9mmeIX33nuPRo0acdddd5ltiqaaBI0Ye/bsSfv27fm///s/s03xOAUFBcyaNYuxY8cSHh5utjmaahI0YgTVd5w7dy6//PKL2aZ4lE8//ZTc3Fwefvhhs03R1ICgEuOIESOoW7cuH330kdmmeJRp06YxZMgQGjdubLYpmhoQVGKsU6cO999/P9OmTaO4otDjfs66devYtGmTdtz4IUElRlBN1SNHjvCf//ynVt+zdetW+vbtS1xcHLGxsfTu3Zt169a5ycpLZ+rUqVx33XV07dq1ynOXLl1KUlISYWFhXrBMUxVBJ8YWLVrQp0+fWg1zbNy4kW7duhEbG8uuXbvYv38/zZs3Jy0tjeXLl7vR2ppx/PhxFixYwOOPP17pefv27aN///48//zznDx50kvWaarEjNxXZrNs2TIB5Mcff6zx35aUlEjbtm0lPj5eLly4ULa/uLhYWrVqJYmJiZKfn+9Oc6vNyy+/LFdccYVcvHix0vOGDRsmb775phQVFUlCQoKEhoZ6yUJNJbgnP6O/UVpaKq1atZJHHnmkxn/7zTffCCCPPfaY07FXXnlFAJk/f747zKwRhYWFkpCQIC+88EKV59r/iGgx+gzpQddMBRV9fOzYsXzyySecOXOmRn+7atUqAK6//nqnY7Z9K1eurL2RNWT+/PmcOHGiWsMZ0dHRXrBIU1OCUoygoo+HhITw8ccf1+jvdu/eDcBVV13ldCwhIQGAPXv21Nq+mjJ16lQGDBhA06ZNvX5tjXsIWjHGxsYyfPhwpk6dSmlpabX/Ljc3F4C6LvLNxcTEAHD69Gm32Fhdtm7dyvr16/Vwhp8TtGIENV913759bvOAijWlgLcDBE+ZMoXk5GTS0tK8el2NewlqMSYnJ3PzzTfXaJgjLi4OgPPnzzsds+2zneMNTp8+zbx583jsscd0lHA/J6jFCKp2XLp0abX7ebZQh0eOHHE6ZgvtkeTFnOXTp08nIiKC++67z2vX1HiGoBdj//79ufrqq/nggw+qdf7NN98MwObNm52O2fZ5K6lMSUkJH3zwAaNHjy7rr2r8GLMHV3yBN954Q+Li4uTcuXNVnltSUiLJycnSpEkTh8H14uJiadOmjSQmJlY56O4uvvjiC7FYLPLTTz9d8nfocUafITjHGcvz8MMPk5+fz6efflrluSEhIcycOZOcnBxGjx7NiRMnOHXqFOPGjSM7O5sZM2YQFRXlBavVcMZtt93m1WaxxnNoMQJXXHEF99xzT7WTrHbt2pX169dz5swZWrVqRdOmTcnOzmb16tX8/ve/94LFsGvXLlatWnVJwxmLFy/GYrFgsVg4evQoJSUlZZ8//PBDD1irqQ4Wqc7TFwRs2bKF6667jtWrV9OzZ0+zzamS8ePH8+WXX7Jnzx5CQvRvagCQof8XrXTs2JEbb7zRL4JW5eXl8cknnzBu3DgtxABC/0/aMX78eBYuXOhy2MKXmDVrFsXFxYwaNcpsUzRuRIvRjiFDhtCwYUPef/99s02pEBFh2rRpjBw5kgYNGphtjsaNaDHaER4ezkMPPcT06dPJz8832xyXLF++nN27d+ukpwGIFmM5xo4dS25uLhkZGWab4pKpU6eSlpZG+/btzTZF42a0GMsRHx/PwIEDmTx5stmmOHHw4EGWLVumV2cEKFqMLhg/fjzff/893377rdmmODB16lQaN25M//79zTZF4wG0GF1w00030alTJ58a5rh48SKzZs3i0Ucf1VHCAxQtxgp45JFH+Oyzz3wmeto///lPzp07x4MPPmi2KRoPocVYAffeey+xsbE+Mz3s/fff55577qFRo0Zmm6LxEFqMFRAdHc0DDzzAe++9R1FRkam2ZGVl8f333zNu3DhT7dB4Fi3GShg3bhwnT55k0aJFptoxdepUunTpQufOnU21Q+NZtBgr4eqrr6Zv376mOnKOHTvGwoUL9XBGEKDFWAXjx48nMzOTH3/80ZTrv//++8TFxTF48GBTrq/xHlqMVdC7d29at27tkGT1yJEjvPjiiwwcONBt1zl69CidO3dm9uzZZVPxCgsLmTFjBmPHjvXagmWNiZgbacA/mDJlitSpU0f+/e9/y6BBgyQ0NFQAadGihduusX37dgHEYrFI/fr15fnnn5fJkydLWFiYHD582G3X0fgs6ToXWBUUFBQQHh5OdHQ0/fv3Jzw8nJKSEoAapwaoDFvgYxHhzJkz/O1vf6OoqIjmzZuzZcsWEhISdCjGAEc3Uyvg559/ZsKECTRq1Ihx48aVicV+mCMvL89t1ysfhbywsBAR4eDBg/Tv358WLVrw7rvvcu7cObddU+Nb6LAbLli5ciW33norFoulrBasiPz8fCIjI2t9zX/+85+MGjWqwlQDFosFEaFZs2b8+OOPOjRj4KHDbriiV69ePPvss9UKTmXLvVFbcnNzCQ0NrfSciIgIPvnkEy3EAEWLsQLefPNN7r///ioF4i4x5uTkVBrPxmKxMHfuXLp37+6W62l8Dy3GCrBYLEyfPp077rij0pz37so4lZubW2FNbLPFnUMpGt9Di7ESQkNDmTt3Lp07d65w2ZI7m6mu+qcWi4U33nhDr9YIArQYqyA6Opply5bRsmVLJ0FaLBa3ifH06dNOYgwJCWHs2LFMmDDBLdfQ+DZajNWgfv36rFixgoYNGzoIMiwszG3N1F9//dXhc1hYGIMHD/apBc4az6LFWE2aNGnC6tWriYmJKetDhoSEuNWBYyM8PJzu3bszZ84cHaQ4iND/0zXgmmuu4auvviI8PLxMJO7sM4ISYnJyMosXL3bL+KXGf9DT4WpI586d+fzzz+nXrx8FBQWc3r4d3nkHDhyAo0fh+HE4eRJyc6G0FPLyoLgY6tSByEiIioK4OIiPh4QEaNIEkpLIs4rxqquuYsWKFXossRocB7ZbXweAo9Z9J4FcoBTIA4qBOkAkEAXEAfFAAtAESAJSgGSgrvfMd0LPwKkuJSWwdStkZkJmJv9ctYqR584xCKhthNVS1K/iFcC3zZvT9JZboEcPSEuDxMRafntgUAJsBTKtr3XAKTdfw4ISZirQA0gDvFj6GVqMlVFYCCtXwhdfwKJF8MsvDof/DnwJLLftCAmBRo2gYUNo0ABCQyE2FsLC4MIFKCiAixchJ0fVomfPAupXvBmQhfqFduDaa2HgQLjrLkhxOhrQFAIrgS+ARcAvlZ9OCNAIaAg0AEKBWNQP3QWgALgI5KBq0bPVsOFaYCBwFy7+b9yLFqNL9u6F6dNh1iz47Tfn46GhSiSdOpFeVMTd990HrVpB48ZKeNXlwgU4dIjj69ax97//JTUvD9avh4oS73TsCGPHwr33QgA3Y/cC04FZgIvSJxQlkk5AO6At0ApoTM36XReAQ8BOYAequbseqCjtUUdgLHAv4IHS12J0YMMGmDQJli2D8sXSrBnceSf06gU33QT163vOjn37ICtL2bFsGZRfqVGvHowZA888o2rhAGEDMAlYBpR/KJsBdwK9gJsAD5Y++1CtlGXWV/l1MvWAMcAzqFrYTWToxcUiIps2idx2m4iSoPFq3FhkwgSR7783z7aLF0UWLRK5+26RiAhH++rWFXnmGZGcHPPscwObROQ2EaHcq7GITBARE0tfLorIIhG5W0QixNG+uiLyjIi4qfTTg1uMp0+LjBsnEhrq+JB37y6Sni5SWGi2hY6cOCHyxhsi8fGO9jZsKPLxxyKlpWZbWCNOi8g4EQkVx4e8u4iki4iPlb6cEJE3RCReHO1tKCIfi0gtSz+IxbhkiUijRo4PdY8eIitXmm1Z1Vy4IDJ5srMo09JE/CRExxIRaSSOD3UPEfGD0pcLIjJZnEWZJiK1KP0gFGNhocjLL4uEhBgPcZMmIrNnm21ZzTl/Xt1LZKRxL/Xrq1rdRykUkZdFJESMh7iJiPhh6ct5UfcSKca91BdVq18CQSbGnByR1FTjwbVYRJ54QuTcObMtqx07d4p06eJ4X6+9ZrZVTuSISKoYD65FRJ4QET8vfdkpIl3E8b4uofSDSIyHDom0bevYz1q61Gyr3EdhochzzznW+I88IlJcbLZlIiJySETaimM/K4BKXwpF5DlxrPEfEZEalH6QiPH4cZEWLYyHNCXFb/pWNeaLL0Sio417ffhh0x07x0WkhRgPaYrUqm/l03whItFi3OvDUm3HThCIMTdXpEMH4+Hs2VN5UQOZtWtFGjQw7nniRNNMyRWRDmI8nD1FeVEDmbUi0kCMe65m6QeBGAcMMB7Krl39v39YXTZsUOOQtnufN88UMwaI8VB2Ff/vH1aXDaLGIW33Xo3STw/sJVTTpqk5pQCtW8PixVDXzHn5XqRLF5g/35ieN2aMWlniRaah5pQCtAYWY+6qCG/SBZiPMT1vDGplSWUErhgPHICnn1bbUVGQng6XX26qSV7nttvgpZfU9pkz4MU4OgcAa+kTBaQDQVb63AZYS58zQFWlH7hinDhRrZAAeOutoFvxUMbEiWAL77hqlWodeOOyqBUSAG/h8RUPLlm6dClJSUmVRvfzNBMBW3DNVajWQUUE5kTxLVugUyfVW2rXTq1DrCL+qafo2rUrV1xxBYu9JAKXbNkC11+vFju3bQvbtoEH83ZsQa2oENSqiq2olRbeYt++ffzxj3/k4MGDHDhwgPPnz1NcXOxFCxzZAlyPWrfaFtiGWjtZjgCNKD5tmrHq4q23TBOiz9CxIwwbprZ37FALpD3INIxVF2/hXSECvPTSS3Tr1o3NmzcTGxvr5as70xGwlj47UIujXRF4Yrx4ETKsa++bN4fbbzfXHl/h8ceN7Y8+8thlLmJEPmgOmFH6M2fOZMKECaY2T8tjV/pUVPqBJ8YVK1T8GVAOC51GTXHDDUa/eeFCFUbEA6xARS4A5bAwo/Sjo6NNuGrl3IDRb16ICiNSnsAT49q1xrauFR2xlUdeHngoLbpd6ZtSK/oytvLIA1yVfuCJceNG9R4TE7we1Iro1s3Y3rDBI5ewlj4xmONB9WXsSh9Xpe87jWp3YYsfk5TkdcdNWFhYhfkcy2cdbtSoESdOnPCGWQbJycZ2RXF2aontW5PwvuPG17ErfZdxdgJPjLYAUlde6fVLu3Kf+8TQhg37SQ+uAm25Adu3er/0fR/7SQ+uSj/wmqm2gX4f7MSbjv1UwPPnPXIJ20C/Ln1n7KcCuir9wBPjZZepdzclpAkoTtmF/fXQ1EBr6aNL3xn7oMuuSj/wxGh7yE6eNNcOX8Q+CHODBh65hO0h06XvjH0QZlelH3hibNVKve/ZoyZHawy++87YbtPGI5ewlj57UJOjNQZ2pY+r0g88MdomRZeWGsMcGsX69cb2jTd65BK2SdGlGMMc3mbx4sVYLBYsFgtHjx6lpKSk7POHH35oklUqWrkNV6UfeBPFv/1WreUDNQPHxML3KfLzVdarnBwVHf3nnz1ymW9Ra/lAzcDRpa/IR2W9ykFFR3dR+gE4UbxzZzXGCPDZZ86h8YOVhQuVEAFGjPDYZTqjxhgBPsM5NH6wshAlRICKSj/wxGixwP33q+1z5+Ddd001xycoLYW331bbFguMGuWxS1mA+63b5wBd+qrJbi19LEBFpR94YgQVYsI2xPH2206p3IKOOXPUmkaAoUPVahYPMgZjiONtqk7lFujMQa1pBBiKWs3iisAUY4MG8MILavvsWXjkEXPtMZOTJ2HCBLUdEQGvvebxSzYArKXPWSCIS5+TgLX0iQAqK/3AFCPA+PEqCBXAggUwY4a59phBaSkMH26Muf7xj9CihVcuPR4VhApgARCEpU8pMBxjzPWPQKWl77lgdT7Ajz+KREWpUIURESJffWW2Rd7lySeNUI2dOokUFHj18j+KSJSoUIURIhJkpS9PihGqsZOIVFH6AR6qMSXFcFwUFsKQIbB5s7k2eYtXX4XJk9X2ZZfBvHmqmepFUjAcF4XAECBISp9XgcnW7cuAeahmaqV4/vfBB3j6aaOGiIkR+fJLsy3yHKWlKjOV7X6jo0Wyskw16WkxaogYEQng0pdSUZmpbPcbLSLVLP0giCguoh7QUaOMBzQy0j9TwFXF+fMiw4YZ9xkRobIem0ypiIwS4wGNFP9MAVcV50VkmBj3GSEq63E1CRIxijjXGCAyYoRIXp7ZlrmHnTtVQh/7FsCyZWZbVUb5GgMRGSEiAVL6slNUQh/7FkANSz+IxGjjnXdEwsKMh7ZlS5Hly8226tLJzxd5/XXHzFOJiSJbtphtmUveEZEwMR7aliLix6Uv+SLyujhmnkoUkS01/6ogFKOIyLp1Ildf7VhLDhkisn+/2ZbVjCVLRJKSHO9jwACRU6fMtqxS1onI1eJYSw4Rkf3mmXRJLBGRJHG8jwEicomlH6RiFFFZjEeOVFl+bQ9yeLjIQw+J/Pyz2dZVzrJlKqOWvQjj4kSmTTM9F2N1yRGRkaKy/Noe5HAReUhEfLz0ZZmojFr2IowTkWlS7VyMrghiMdrIynLsa4HK/tu7t0h6us9k/pUzZ0Q++MAx16R9rX7ihNkWXhJZ4tjXQlT2394iki41yvzrUc6IyAfimGvSvlZ3Q+lrMYqISFGRyMcfi1xzjfOD3rSpyLPPqnyH3q51zp0TycgQuecex1yLoGr0fv1ENm3yrk0eoEhEPhaRa8T5QW8qIs+Kynfo7Tr/nIhkiMg94phrEVE1ej8RcWPppwfeesbaUFwMc+fC1KlqXWR5EhLgllugRw9ITTWiCriLggK1Gj8zE9asgawsI8CWjchIGDQInnpKJfcJIIqBucBU1LrI8iQAtwA9gFSMqALuogC1Gj8TWANkYQTYshEJDAKeQiX3cSMZWowVsWULTJ+uEo5WFNawXj0VizQlRa2hjI+HxERo1Egdi4pSEdkiItRyrqIiNXH97Fk4fFjNGT14EHbuhO3bITtb/SAAe4Fr7K/Vrh2MHAmjR8MVV3j67k1nCzAdlXC0oqCS9VCxSFNQayjjgUSgkfVYFCoiWwRqOVcRauL6WeAwas7oQWAnsB3IRv0guKIdMBIYDXio9LUYq6SkRNVUCxbAV1/B3r0ev+R/Ub/861JS6HLvvTBwoLFgOsgoQdVUC4CvUD9SFVJY6LYpf+GohdL9gIEYC6Y9iBZjjTl2TIlz/XpVm23b5hgC8VIICYGmTVUN26EDpKbSa9IkcvPy+PbbbwkN9pR2dhxDiXM9qjbbhjUEYn4+NGwIn34K/frV6DtDgKaoGrYD6oewK15Pea7F6BZOnFAxZY4fh6NHVfMzL0/1AS9cUO+xsRAWpnKA1Kun+p/x8eq9ZUvHAMPAzp076dChA//4xz8YO3asSTfmH5wAPs/MZHxaGn/++WeKmzUjD9UHvGB9j0WFz49BNWETUM3aBKAlXheeKzICL7y/GTRurF5uJDk5mSeeeIIXXniBQYMGcaUJ6Qr8hcbAqcxMEhMT+UuzZmabc8kE9hIqP+fPf/4zderU4QVb1AJNhWRmZpKWlma2GbVCi9GHiY2N5a9//SsfffQRGzyUwi0QKCwsZMOGDfTs2dNsU2qF7jP6Ab169SI3N1c7cypg7dq1pKamkp2dzTXXXFP1H/gmARg3NQCZMmUK27ZtY0YwxvGpBpmZmcTHx/uzEAHdTPUL7J05v/76q9nm+ByZmZncfPPNZptRa7QY/QTtzHFNcXFxQPQXQYvRb9DOHNds2rSJvLw8LUaNd7nnnntIS0tj3LhxlJSUmG2OT7B69WoaN25MUgBMF9Ri9DO0M8eRzMxMevbsicViMduUWqPF6GdoZ45BcXEx69evD4gmKmgx+iXamaPYsmULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", + "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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", "text/plain": [ "" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "hnp.draw_graph(homomorphic_model).show()" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "759bc39c", - "metadata": {}, "source": [ "### It's time to compile the function to its homomorphic equivalent" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 23, - "id": "5c62c8b2", - "metadata": {}, - "outputs": [], "source": [ "engine = hnp.compile_numpy_function(\n", " infer,\n", " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", - " iter(inputset),\n", + " inputset,\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "2d6865f7", - "metadata": {}, "source": [ "### Finally, let's make homomorphic inference" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 24, - "id": "29374aa5", - "metadata": {}, - "outputs": [], "source": [ "homomorphic_predictions = []\n", "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", " inference = QuantizedArray(engine.run(x_i), y_q.parameters)\n", " homomorphic_predictions.append(inference.dequantize())\n", "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "420b654c", - "metadata": {}, "source": [ "### And visualize it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 25, - "id": "1fc3156e", - "metadata": {}, + "source": [ + "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", + "display(fig)" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", - "display(fig)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "1692814f", - "metadata": {}, "source": [ "### Enjoy!" - ] + ], + "metadata": {} } ], "metadata": {}, diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index 230a195a1..3ea336579 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -2,109 +2,86 @@ "cells": [ { "cell_type": "markdown", - "id": "9ea014b3", - "metadata": {}, "source": [ "# Quantized Logistic Regression\n", "\n", "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "d341fd23", - "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our logistic regression model" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 1, - "id": "0a7429ff", - "metadata": {}, - "outputs": [], "source": [ "import numpy as np\n", "import torch" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "abfeea1b", - "metadata": {}, "source": [ "### And some helpers for visualization" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 2, - "id": "a3f970f3", - "metadata": {}, - "outputs": [], "source": [ "%matplotlib inline\n", "\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "c7a0cc5f", - "metadata": {}, "source": [ "### We need an inputset, a handcrafted one for simplicity" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 3, - "id": "809023a3", - "metadata": {}, - "outputs": [], "source": [ "x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n", "y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "2d522cb0", - "metadata": {}, "source": [ "### Let's visualize our inputset to get a grasp of it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 4, - "id": "4cda7fe2", - "metadata": {}, - "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 5, - "id": "9b34b9a4", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "x_min, x_max = x[:, 0].min(), x[:, 0].max()\n", "x_deviation = x_max - x_min\n", @@ -128,22 +105,31 @@ " color=\"blue\",\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAPTklEQVR4nO3df4zkd13H8efruPPHpcgZbqO117v1D1ABKbQr1ED0lCgHmBJjTagVbCO5RKsup4mNEOkpaaIhchQbOC6lOdT1wNAGSgNGImAlhJo9LO2VCmmEKweNt7S5omBMznv7x3eW7q27O7N3szuzn30+ksnO9/v93Hxf/XTvtd/5zMxtqgpJ0sa3ZdQBJEnDYaFLUiMsdElqhIUuSY2w0CWpEVtHdeKdO3fW5OTkqE4vSRvS8ePHv1lVE0sdG1mhT05OMjs7O6rTS9KGlOTkcsdccpGkRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSI9ov9MW/M9XfoSqpUX0LPcnlST6V5ItJHk4yvcSYJHlXkkeTPJjkyrWJu0oHD8KBA0+XeFW3ffDgKFNJQzczA5OTsGVL93VmZtSJ2jeOcz7IFfpZ4A+q6nnA1cBNSZ63aMyrgOf0bvuB9ww15YWogjNn4Lbbni71Awe67TNnvFJXM2ZmYP9+OHmy+7Y+ebLbHoeCadW4znlqlcWW5CPA7VX1iQX73gt8uqqO9ba/BOytqseXe5ypqala838PfWGJz5uehkOHIFnbc0vrZHKyK5TF9uyBr351vdNsDqOc8yTHq2pqqWOrWkNPMgm8GLh/0aHLgK8t2D7V27f4z+9PMptkdm5ubjWnvjBJV94LWeZqzGOPrW6/Lt64zvnAhZ7kEuAu4E1V9a0LOVlVHamqqaqamphY8jcoDdf8FfpCC9fUpQbs3r26/bp44zrnAxV6km10ZT5TVXcvMeTrwOULtnf19o3OwuWW6Wk4d677unBNXWrArbfC9u3n79u+vduvtTGucz7Iu1wCvA94pKrescywe4A39N7tcjXw1Err5+sigR07zl8zP3So296xw2UXNeP66+HIkW79Num+HjnS7dfaGNc57/uiaJKXA/8MPASc6+1+M7AboKoO90r/dmAf8B3gxqpa8RXPdXlRtAt4fnkv3pakDWSlF0W39vvDVfUZYMUGrO6nwk0XFm+NLS5vy1xSo9r/pKgkbRIWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUiL6FnuTOJKeTnFjm+LOSfDTJF5I8nOTG4ceUJPUzyBX6UWDfCsdvAr5YVVcAe4G/SPI9Fx9NkrQafQu9qu4DnlxpCPDMJAEu6Y09O5x4kqRBDWMN/XbgJ4BvAA8B01V1bqmBSfYnmU0yOzc3N4RTS5LmDaPQXwk8APwI8CLg9iQ/sNTAqjpSVVNVNTUxMTGEU0uS5g2j0G8E7q7Oo8BXgB8fwuNKklZhGIX+GPAKgCQ/BPwY8O9DeFxJ0ips7TcgyTG6d6/sTHIKuAXYBlBVh4G3AUeTPAQEuLmqvrlmiSVJS+pb6FV1XZ/j3wB+cWiJJEkXxE+KSlIjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmN6FvoSe5McjrJiRXG7E3yQJKHk/zTcCNKkgYxyBX6UWDfcgeT7ADeDVxTVc8HfnUoySRJq9K30KvqPuDJFYb8GnB3VT3WG396SNkkSaswjDX05wI/mOTTSY4necNyA5PsTzKbZHZubm4Ip5YkzRtGoW8FrgJeA7wS+OMkz11qYFUdqaqpqpqamJgYwqklSfO2DuExTgFPVNW3gW8nuQ+4AvjyEB5bkjSgYVyhfwR4eZKtSbYDLwUeGcLjSpJWoe8VepJjwF5gZ5JTwC3ANoCqOlxVjyT5e+BB4BxwR1Ut+xZHSdLa6FvoVXXdAGPeDrx9KIkkSRfET4pKUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY3oW+hJ7kxyOsmJPuN+KsnZJNcOL54kaVCDXKEfBfatNCDJM4A/B/5hCJkkSRegb6FX1X3Ak32G/S5wF3B6GKEkSat30WvoSS4Dfhl4zwBj9yeZTTI7Nzd3saeWJC0wjBdF3wncXFXn+g2sqiNVNVVVUxMTE0M4tSRp3tYhPMYU8IEkADuBVyc5W1UfHsJjS5IGdNGFXlU/On8/yVHgXstcktZf30JPcgzYC+xMcgq4BdgGUFWH1zSdJGlgfQu9qq4b9MGq6oaLSiNJumB+UlSSGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJakTfQk9yZ5LTSU4sc/z6JA8meSjJZ5NcMfyYkqR+BrlCPwrsW+H4V4CfraqfBN4GHBlCLknSKm3tN6Cq7ksyucLxzy7Y/Bywawi5JEmrNOw19N8EPr7cwST7k8wmmZ2bmxvyqSVpcxtaoSf5ObpCv3m5MVV1pKqmqmpqYmJiWKeWJDHAkssgkrwQuAN4VVU9MYzHlCStzkVfoSfZDdwNvL6qvnzxkSRJF6LvFXqSY8BeYGeSU8AtwDaAqjoMvBV4NvDuJABnq2pqrQJLkpY2yLtcrutz/I3AG4eWSJJ0QfykqCQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUiPYLvWrlbQ2fcy6NRN9CT3JnktNJTixzPEneleTRJA8muXL4MS/QwYNw4MDThVLVbR88OMpUbXPOtUnMzMDkJGzZ0n2dmRl1osGu0I8C+1Y4/irgOb3bfuA9Fx9rCKrgzBm47banC+bAgW77zBmvGteCc65NYmYG9u+Hkye7b+uTJ7vtkZd6VfW9AZPAiWWOvRe4bsH2l4BL+z3mVVddVWvu3Lmq6emqbs672/R0t19rwznXJrBnz/nf4vO3PXvW/tzAbC3Tq6kBrpqSTAL3VtULljh2L/BnVfWZ3vY/AjdX1ewSY/fTXcWze/fuq06ePHkBP4JWqap7TjTv3DlI1v68m5lzrsZt2bL0E86k+3ZfS0mOV9XUkrnW9tTnq6ojVTVVVVMTExPrccLuKf9CC9d3NXzOuTaB3btXt3+9DKPQvw5cvmB7V2/faC1cv52e7n5sTk+fv76r4XLOtUnceits337+vu3bu/2jtHUIj3EP8DtJPgC8FHiqqh4fwuNenAR27OgK5dChbvvQoe7Yjh0uAawF51ybxPXXd1/f8hZ47LHuyvzWW5/ePyp919CTHAP2AjuB/wBuAbYBVNXhJAFup3snzHeAG5daP19samqqZmf7Drt4VecXyeJtDZ9zLq2ZldbQ+16hV9V1fY4XcNMFZlt7i4vEYll7zrk0Eu1/UlSSNgkLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJasRAv7FoTU6czAHr8CuLvmsn8M11PN8wbdTsGzU3bNzsGzU3bNzs6517T1Ut+RuCRlbo6y3J7HL/5OS426jZN2pu2LjZN2pu2LjZxym3Sy6S1AgLXZIasZkK/cioA1yEjZp9o+aGjZt9o+aGjZt9bHJvmjV0SWrdZrpCl6SmWeiS1IimCj3JnUlOJzmxzPEkeVeSR5M8mOTK9c64nAGy703yVJIHere3rnfGpSS5PMmnknwxycNJppcYM3bzPmDucZ3z70vyL0m+0Mv+J0uM+d4kH+zN+f1JJkcQdXGmQXLfkGRuwZy/cRRZl5PkGUn+Ncm9Sxwb/ZxXVTM34GeAK4ETyxx/NfBxIMDVwP2jzryK7HuBe0edc4lclwJX9u4/E/gy8Lxxn/cBc4/rnAe4pHd/G3A/cPWiMb8NHO7dfx3wwQ2S+wbg9lFnXeG/4feBv13q+2Ic5rypK/Squg94coUhrwX+qjqfA3YkuXR90q1sgOxjqaoer6rP9+7/J/AIcNmiYWM37wPmHku9efyv3ua23m3xuxteC7y/d/9DwCuSZJ0iLmnA3GMryS7gNcAdywwZ+Zw3VegDuAz42oLtU2yQv8Q9P917uvrxJM8fdZjFek8xX0x35bXQWM/7CrlhTOe899T/AeA08ImqWnbOq+os8BTw7HUNuYQBcgP8Sm9p7kNJLl/fhCt6J/CHwLlljo98zjdboW9kn6f7NxyuAP4S+PBo45wvySXAXcCbqupbo84zqD65x3bOq+p/q+pFwC7gJUleMOJIAxkg90eByap6IfAJnr7iHakkvwScrqrjo86yks1W6F8HFv7E39XbN/aq6lvzT1er6mPAtiQ7RxwLgCTb6EpxpqruXmLIWM57v9zjPOfzquoM8Clg36JD353zJFuBZwFPrGu4FSyXu6qeqKr/6W3eAVy1ztGW8zLgmiRfBT4A/HySv1k0ZuRzvtkK/R7gDb13XVwNPFVVj4861CCS/PD8elySl9D9vxv5X9BepvcBj1TVO5YZNnbzPkjuMZ7ziSQ7eve/H/gF4N8WDbsH+I3e/WuBT1bv1bpRGST3otdWrqF7bWPkquqPqmpXVU3SveD5yar69UXDRj7nW9fzZGstyTG6dybsTHIKuIXuhReq6jDwMbp3XDwKfAe4cTRJ/78Bsl8L/FaSs8B/A68b9V/QnpcBrwce6q2NArwZ2A1jPe+D5B7XOb8UeH+SZ9D9kPm7qro3yZ8Cs1V1D90Pq79O8ijdi+2vG13c7xok9+8luQY4S5f7hpGlHcC4zbkf/ZekRmy2JRdJapaFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhrxf09l6LOTuZAtAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "72076b9c", - "metadata": {}, "source": [ "### Now, we need a model so let's define it" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 6, - "id": "ae24e6a8", - "metadata": {}, - "outputs": [], "source": [ "class Model(torch.nn.Module):\n", " def __init__(self, n):\n", @@ -153,27 +139,45 @@ " def forward(self, x):\n", " output = torch.sigmoid(self.fc(x))\n", " return output" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "8d67d2a6", - "metadata": {}, "source": [ "### And create one\n", "\n", "The main purpose of this tutorial is not to train a logistic regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 7, - "id": "72440f47", - "metadata": {}, + "source": [ + "model = Model(x.shape[1])\n", + "\n", + "optimizer = torch.optim.SGD(model.parameters(), lr=1)\n", + "criterion = torch.nn.BCELoss()\n", + "\n", + "epochs = 1501\n", + "for e in range(1, epochs + 1):\n", + " optimizer.zero_grad()\n", + "\n", + " out = model(x)\n", + " loss = criterion(out, y)\n", + "\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " if e % 100 == 1 or e == epochs:\n", + " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "Epoch: 1 | Loss: 0.568401038646698\n", "Epoch: 101 | Loss: 0.13618899881839752\n", @@ -194,40 +198,18 @@ ] } ], - "source": [ - "model = Model(x.shape[1])\n", - "\n", - "optimizer = torch.optim.SGD(model.parameters(), lr=1)\n", - "criterion = torch.nn.BCELoss()\n", - "\n", - "epochs = 1501\n", - "for e in range(1, epochs + 1):\n", - " optimizer.zero_grad()\n", - "\n", - " out = model(x)\n", - " loss = criterion(out, y)\n", - "\n", - " loss.backward()\n", - " optimizer.step()\n", - "\n", - " if e % 100 == 1 or e == epochs:\n", - " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "b948f03b", - "metadata": {}, "source": [ "### Time to make some predictions" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 8, - "id": "086ad98b", - "metadata": {}, - "outputs": [], "source": [ "contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 100)\n", "contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 100)\n", @@ -235,33 +217,20 @@ "\n", "inputs = np.stack((contour_plot_x_data.flatten(), contour_plot_y_data.flatten()), axis=1)\n", "predictions = model(torch.tensor(inputs).float()).detach().numpy()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "e82c642f", - "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 9, - "id": "8b4b09d4", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "contour = ax.contourf(\n", " contour_plot_x_data,\n", @@ -271,25 +240,42 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "fc8f7add", - "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 10, - "id": "6f961c04", - "metadata": {}, + "source": [ + "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", + "b = model.fc.bias.flatten().tolist()[0]\n", + "\n", + "print(w)\n", + "print(b)" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "[[4.53581047]\n", " [2.3700912 ]]\n", @@ -297,71 +283,56 @@ ] } ], - "source": [ - "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", - "b = model.fc.bias.flatten().tolist()[0]\n", - "\n", - "print(w)\n", - "print(b)" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "1e25c552", - "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "469c400f", - "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 11, - "id": "669f761a", - "metadata": {}, + "source": [ + "from IPython.display import SVG\n", + "SVG(filename=\"figures/QuantizationVisualized.svg\")" + ], "outputs": [ { + "output_type": "execute_result", "data": { - "image/svg+xml": [ - "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" - ], + "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
", "text/plain": [ "" ] }, - "execution_count": 11, "metadata": {}, - "output_type": "execute_result" + "execution_count": 11 } ], - "source": [ - "from IPython.display import SVG\n", - "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "fc365b14", - "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 12, - "id": "e95d696c", - "metadata": {}, - "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -515,66 +486,60 @@ " def apply(self, x):\n", " assert x.parameters == self.input_parameters\n", " return QuantizedArray(self.table[x.values], self.output_parameters)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "500a4168", - "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 13, - "id": "286dd2be", - "metadata": {}, - "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(w, parameter_bits)\n", "b_q = QuantizedArray.of(b, parameter_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "b3914f63", - "metadata": {}, "source": [ "### And quantize our inputs" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 14, - "id": "22ca3dd2", - "metadata": {}, - "outputs": [], "source": [ "input_bits = 5\n", "\n", "x = inputs\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "e8faf37f", - "metadata": {}, "source": [ "### Time to make quantized inference" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 15, - "id": "e882daa3", - "metadata": {}, - "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -585,33 +550,20 @@ "y_q = sigmoid.apply(intermediate_q)\n", "\n", "quantized_predictions = y_q.dequantize()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "4e94b1ce", - "metadata": {}, "source": [ "### And visualize the results" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 16, - "id": "8ad8b4f7", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -624,22 +576,31 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "80f6ce01", - "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 17, - "id": "db457e1f", - "metadata": {}, - "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (intermediate.max() - intermediate.min())\n", "zp_y = int(round(intermediate.min() * q_y))\n", @@ -655,12 +616,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "9c5bbd90", - "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -677,32 +638,28 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ] + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "568ef254", - "metadata": {}, "source": [ "### Let's import the concrete numpy package now!" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 18, - "id": "668d59ba", - "metadata": {}, - "outputs": [], "source": [ "import concrete.numpy as hnp" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": 19, - "id": "6e8f3272", - "metadata": {}, - "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -727,22 +684,20 @@ "\n", "def infer(x_0, x_1):\n", " return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)]" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "45d86243", - "metadata": {}, "source": [ "### Let's compile our quantized inference function to it's operation graph for visualization" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 20, - "id": "a612859a", - "metadata": {}, - "outputs": [], "source": [ "inputset = []\n", "for x_i in x_q:\n", @@ -754,27 +709,29 @@ " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " },\n", - " iter(inputset),\n", + " inputset,\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "cf6d3de7", - "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 21, - "id": "e79486c8", - "metadata": {}, + "source": [ + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ], "outputs": [ { - "name": "stdout", "output_type": "stream", + "name": "stdout", "text": [ "%0 = Constant(2) # ClearScalar>\n", "%1 = Constant(1) # ClearScalar>\n", @@ -793,45 +750,38 @@ ] } ], - "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" - ] + "metadata": {} }, { "cell_type": "code", "execution_count": 22, - "id": "8d937cec", - "metadata": {}, + "source": [ + "hnp.draw_graph(homomorphic_model).show()" + ], "outputs": [ { + "output_type": "display_data", "data": { - "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", + "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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", "text/plain": [ "" ] }, - "metadata": {}, - "output_type": "display_data" + "metadata": {} } ], - "source": [ - "hnp.draw_graph(homomorphic_model).show()" - ] + "metadata": {} }, { "cell_type": "markdown", - "id": "ed9eabc3", - "metadata": {}, "source": [ "### It's time to compile the function to its homomorphic equivalent" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 23, - "id": "6df86e59", - "metadata": {}, - "outputs": [], "source": [ "engine = hnp.compile_numpy_function(\n", " infer,\n", @@ -839,39 +789,22 @@ " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", " },\n", - " iter(inputset),\n", + " inputset,\n", ")" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "markdown", - "id": "ea82278b", - "metadata": {}, "source": [ "### Finally, let's make homomorphic inference" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": 24, - "id": "790377e4", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d0e8ac9eb4174f29978c3d4e4b6f42c9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/10000 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -921,15 +856,27 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ] + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {} + } + ], + "metadata": {} }, { "cell_type": "markdown", - "id": "a3d368e7", - "metadata": {}, "source": [ "### Enjoy!" - ] + ], + "metadata": {} } ], "metadata": {}, diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index 834bb68e1..e349bdba7 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -39,7 +39,7 @@ Finally, we can compile our function to its homomorphic equivalent. ```python engine = hnp.compile_numpy_function( f, {"x": x, "y": y}, - inputset=iter(inputset), + inputset=inputset, ) ``` diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 64cc695b5..4ff9b9c15 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -22,7 +22,7 @@ def test_artifacts_export(): compile_numpy_function( function, {"x": EncryptedScalar(UnsignedInteger(7))}, - iter([(0,), (1,), (2,)]), + [(0,), (1,), (2,)], compilation_artifacts=artifacts, ) diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index c8619854b..fde2131bd 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -49,7 +49,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused param: EncryptedScalar(Integer(32, is_signed=False)) for param in signature(function_to_trace).parameters.keys() }, - iter([(1,), (2,), (3,)]), + [(1,), (2,), (3,)], CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) op_graph_not_optimized = compile_numpy_function_into_op_graph( @@ -58,7 +58,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused param: EncryptedScalar(Integer(32, is_signed=False)) for param in signature(function_to_trace).parameters.keys() }, - iter([(1,), (2,), (3,)]), + [(1,), (2,), (3,)], CompilationConfiguration( dump_artifacts_on_unexpected_failures=False, enable_topological_optimizations=False, diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py index 4155cbb94..c8396e619 100644 --- a/tests/common/debugging/test_drawing.py +++ b/tests/common/debugging/test_drawing.py @@ -18,7 +18,7 @@ def test_draw_graph_with_saving(): op_graph = compile_numpy_function_into_op_graph( function, {"x": EncryptedScalar(Integer(7, True))}, - iter([(-2,), (-1,), (0,), (1,), (2,)]), + [(-2,), (-1,), (0,), (1,), (2,)], ) with tempfile.TemporaryDirectory() as tmp: diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 73f6e2388..1036b0b91 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -198,7 +198,7 @@ def test_compile_function_with_direct_tlu(): op_graph = compile_numpy_function_into_op_graph( function, {"x": EncryptedScalar(Integer(2, is_signed=False))}, - iter([(0,), (1,), (2,), (3,)]), + [(0,), (1,), (2,), (3,)], ) str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) @@ -217,7 +217,7 @@ def test_compile_function_with_direct_tlu_overflow(): compile_numpy_function_into_op_graph( function, {"x": EncryptedScalar(Integer(3, is_signed=False))}, - iter([(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)]), + [(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)], CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) From 25fafcd4f4673db8ccb7b962d4a5d60692707756 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 16 Sep 2021 10:38:53 +0300 Subject: [PATCH 0252/1104] chore: add curl back to docker for benchmarks --- docker/Dockerfile.concretefhe-env | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index 5acd26e7c..78a1015d9 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -2,6 +2,7 @@ FROM ghcr.io/zama-ai/zamalang-compiler RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ + curl \ python3.8 \ python3.8-tk \ python3.8-venv \ From 407e57c3845d96661d53579432dbb9aa9969d2ee Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 16 Sep 2021 10:41:03 +0300 Subject: [PATCH 0253/1104] chore: make docker_publish_measurements target work better locally, improve daily benchmarks workflow --- .github/workflows/daily-benchmarks.yaml | 2 ++ Makefile | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/daily-benchmarks.yaml b/.github/workflows/daily-benchmarks.yaml index 1899e9da6..ff9ddf6e5 100644 --- a/.github/workflows/daily-benchmarks.yaml +++ b/.github/workflows/daily-benchmarks.yaml @@ -41,7 +41,9 @@ jobs: key: ${{ secrets.BENCHMARKS_EC2_SSH_KEY }} script: | cd ~/concretefhe-internal + git pull make docker_publish_measurements + docker system prune -f - name: Write SSH Key To A File run: echo "$SSH_KEY" > ~/ssh-key && chmod 400 ~/ssh-key diff --git a/Makefile b/Makefile index c438c5af0..7d3b3b715 100644 --- a/Makefile +++ b/Makefile @@ -133,7 +133,6 @@ docker_bas: docker_build_and_start .PHONY: docker_bas docker_publish_measurements: docker_build - git pull mkdir -p .benchmarks python script/progress_tracker_utils/extract_machine_info.py docker run --rm --volume /"$$(pwd)":/src $(DEV_DOCKER_IMG) \ From 1c82db27067d5d8c794dc3dbfd3459e4a0502a74 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 15 Sep 2021 17:40:32 +0200 Subject: [PATCH 0254/1104] test: adding tests on script closes #288 --- Makefile | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 7d3b3b715..d0455f3e8 100644 --- a/Makefile +++ b/Makefile @@ -19,12 +19,12 @@ sync_env: python_format: poetry run env bash ./script/source_format/format_python.sh \ - --dir $(SRC_DIR) --dir tests --dir benchmarks + --dir $(SRC_DIR) --dir tests --dir benchmarks --dir script .PHONY: python_format check_python_format: poetry run env bash ./script/source_format/format_python.sh \ - --dir $(SRC_DIR) --dir tests --dir benchmarks --check + --dir $(SRC_DIR) --dir tests --dir benchmarks --dir script --check .PHONY: check_python_format check_strip_nb: @@ -32,7 +32,7 @@ check_strip_nb: .PHONY: check_strip_nb pylint: - $(MAKE) --keep-going pylint_src pylint_tests pylint_benchmarks + $(MAKE) --keep-going pylint_src pylint_tests pylint_benchmarks pylint_script .PHONY: pylint pylint_src: @@ -50,9 +50,13 @@ pylint_benchmarks: --rcfile=pylintrc benchmarks .PHONY: pylint_benchmarks +pylint_script: + poetry run pylint --rcfile=pylintrc script +.PHONY: pylint_script + flake8: poetry run flake8 --max-line-length 100 --per-file-ignores="__init__.py:F401" \ - $(SRC_DIR)/ tests/ benchmarks/ + $(SRC_DIR)/ tests/ benchmarks/ script/ .PHONY: flake8 python_linting: pylint flake8 @@ -93,11 +97,15 @@ mypy_benchmark: find ./benchmarks/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports .PHONY: mypy_benchmark +mypy_script: + find ./script/ -name "*.py" | xargs poetry run mypy --ignore-missing-imports +.PHONY: mypy_script + # The plus indicates that make will be called by the command and allows to share the context with # the parent make execution. We serialize calls to these targets as they may overwrite each others # cache which can cause issues. mypy_ci: - $(MAKE) --keep-going mypy mypy_test mypy_benchmark + $(MAKE) --keep-going mypy mypy_test mypy_benchmark mypy_script .PHONY: mypy_ci pytest_and_coverage: pytest coverage From ef9200793de04f9f712f1226e76ed0c0286e119c Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 15 Sep 2021 17:43:01 +0200 Subject: [PATCH 0255/1104] fix: flake8 for script --- .../actions_utils/coverage_report_format.py | 2 +- script/progress_tracker_utils/measure.py | 27 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/script/actions_utils/coverage_report_format.py b/script/actions_utils/coverage_report_format.py index 0d8340529..443ea1f95 100755 --- a/script/actions_utils/coverage_report_format.py +++ b/script/actions_utils/coverage_report_format.py @@ -41,5 +41,5 @@ if __name__ == "__main__": cli_args = parser.parse_args() try: main(cli_args) - except Exception as e: + except Exception: traceback.print_exc() diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index c8ff04c56..03d1db4ed 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -107,7 +107,7 @@ def create_modified_script(script_without_extension, lines, metrics): # Create a measurement dictionary to accumulate values f.write("_measurements_ = {\n") for metric_id in metrics.keys(): - f.write(f" \"{metric_id}\": [],\n") + f.write(f' "{metric_id}": [],\n') f.write("}\n") # Create a variable to hold the id of the current metric @@ -129,7 +129,7 @@ def create_modified_script(script_without_extension, lines, metrics): f.write(f"{line}_end_ = time.time()\n") value = "(_end_ - _start_) * 1000" - line += f"_measurements_[\"{current_metric_id}\"].append({value})\n" + line += f'_measurements_["{current_metric_id}"].append({value})\n' elif line.strip().startswith("# Measure:"): # Replace `# Measure: ...` with # @@ -151,15 +151,15 @@ def create_modified_script(script_without_extension, lines, metrics): line += "_start_ = time.time()\n" else: value = metric_details[1] - line += f"_measurements_[\"{metric_id}\"].append({value.strip()})\n" + line += f'_measurements_["{metric_id}"].append({value.strip()})\n' # Write the possibly replaced line back f.write(line) # Dump measurements to a temporary file after the script is executed from start to end f.write("\n") - f.write(f"with open(\"{script_without_extension}.measurements\", \"w\") as f:\n") - f.write(f" json.dump(_measurements_, f, indent=2)\n") + f.write(f'with open("{script_without_extension}.measurements", "w") as f:\n') + f.write(" json.dump(_measurements_, f, indent=2)\n") def perform_measurements(script, script_without_extension, target_id, metrics, samples, result): @@ -213,7 +213,7 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s for measurement in results[metric_id]: measurements[metric_id].append(measurement) - pbar.write(f"") + pbar.write("") pbar.update(1) print() @@ -222,10 +222,9 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s if working: # Take average of all metrics and store them in `result` - result["targets"][target_id]["measurements"].update({ - metric_id: sum(metric) / len(metric) - for metric_id, metric in measurements.items() - }) + result["targets"][target_id]["measurements"].update( + {metric_id: sum(metric) / len(metric) for metric_id, metric in measurements.items()} + ) # Add metrics of the current script to the result for metric_id, metric_label in metrics.items(): @@ -241,7 +240,7 @@ def main(): parser.add_argument("base", type=str, help="directory which contains the benchmarks") parser.add_argument("--samples", type=int, default=30, help="number of samples to take") - parser.add_argument("--keep", action='store_true', help="flag to keep measurement scripts") + parser.add_argument("--keep", action="store_true", help="flag to keep measurement scripts") args = parser.parse_args() @@ -274,9 +273,9 @@ def main(): print("-" * len(str(script))) with tqdm.tqdm(total=samples) as pbar: - pbar.write(f" Sample 1") - pbar.write(f" --------") - pbar.write(f" Skipped (doesn't have a `# Target:` directive)\n") + pbar.write(" Sample 1") + pbar.write(" --------") + pbar.write(" Skipped (doesn't have a `# Target:` directive)\n") pbar.update(samples) print() From fe03521dc6dd46b4278fca97181bc1ecf10aa994 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 15 Sep 2021 17:45:14 +0200 Subject: [PATCH 0256/1104] fix: python_format --- script/nbmake_utils/notebook_sanitize.py | 7 +++---- script/progress_tracker_utils/extract_machine_info.py | 7 ++++--- script/progress_tracker_utils/measure.py | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/script/nbmake_utils/notebook_sanitize.py b/script/nbmake_utils/notebook_sanitize.py index adcd4499e..4dc48b6b7 100644 --- a/script/nbmake_utils/notebook_sanitize.py +++ b/script/nbmake_utils/notebook_sanitize.py @@ -1,14 +1,13 @@ import argparse import json - from pathlib import Path def main(): - parser = argparse.ArgumentParser(description='Sanitizer for Jupyter Notebooks') + parser = argparse.ArgumentParser(description="Sanitizer for Jupyter Notebooks") - parser.add_argument('base', type=str, help='directory which contains the notebooks') - parser.add_argument('--check', action='store_true', help='flag to enable just checking mode') + parser.add_argument("base", type=str, help="directory which contains the notebooks") + parser.add_argument("--check", action="store_true", help="flag to enable just checking mode") args = parser.parse_args() diff --git a/script/progress_tracker_utils/extract_machine_info.py b/script/progress_tracker_utils/extract_machine_info.py index 0f81185a7..ea05b8025 100644 --- a/script/progress_tracker_utils/extract_machine_info.py +++ b/script/progress_tracker_utils/extract_machine_info.py @@ -1,11 +1,12 @@ -import cpuinfo -import dotenv import json import os import platform -import psutil import urllib.parse +import cpuinfo +import dotenv +import psutil + def main(): dotenv.load_dotenv() diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 03d1db4ed..e263f4a12 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -4,6 +4,7 @@ import os import pathlib import subprocess import urllib + import tqdm From af41980f1f37c69dc92c63568a317e91da7951eb Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 15 Sep 2021 18:03:07 +0200 Subject: [PATCH 0257/1104] fix: pylint fix: pylint --- script/actions_utils/coverage_report_format.py | 2 +- script/nbmake_utils/notebook_sanitize.py | 8 +++++--- .../progress_tracker_utils/extract_machine_info.py | 4 +++- script/progress_tracker_utils/measure.py | 13 ++++++++----- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/script/actions_utils/coverage_report_format.py b/script/actions_utils/coverage_report_format.py index 443ea1f95..a0609b876 100755 --- a/script/actions_utils/coverage_report_format.py +++ b/script/actions_utils/coverage_report_format.py @@ -12,7 +12,7 @@ def main(args): diff_cover_content = None - with open(diff_cover_file_path, "r") as f: + with open(diff_cover_file_path, "r", encoding="utf-8") as f: diff_cover_content = f.readlines() with open(diff_cover_file_path, "w", encoding="utf-8") as f: diff --git a/script/nbmake_utils/notebook_sanitize.py b/script/nbmake_utils/notebook_sanitize.py index 4dc48b6b7..c26ee33f5 100644 --- a/script/nbmake_utils/notebook_sanitize.py +++ b/script/nbmake_utils/notebook_sanitize.py @@ -1,9 +1,11 @@ +"""Sanitizer for Jupyter notebooks.""" import argparse import json from pathlib import Path def main(): + """Sanitize""" parser = argparse.ArgumentParser(description="Sanitizer for Jupyter Notebooks") parser.add_argument("base", type=str, help="directory which contains the notebooks") @@ -15,16 +17,16 @@ def main(): notebooks = base.glob("*.ipynb") for notebook in notebooks: - with open(notebook, "r") as f: + with open(notebook, "r", encoding="utf-8") as f: content = json.load(f) if args.check: if len(content["metadata"]) != 0: print("Notebooks are not sanitized. Please run `make conformance`.") - exit(1) + raise ValueError else: content["metadata"] = {} - with open(notebook, "w", newline="\n") as f: + with open(notebook, "w", newline="\n", encoding="utf-8") as f: json.dump(content, f, indent=1, ensure_ascii=False) f.write("\n") diff --git a/script/progress_tracker_utils/extract_machine_info.py b/script/progress_tracker_utils/extract_machine_info.py index ea05b8025..c8bd4e5df 100644 --- a/script/progress_tracker_utils/extract_machine_info.py +++ b/script/progress_tracker_utils/extract_machine_info.py @@ -1,3 +1,4 @@ +"""Extract some info about the host machine.""" import json import os import platform @@ -9,6 +10,7 @@ import psutil def main(): + """Extract some info about the host machine.""" dotenv.load_dotenv() properties = [] @@ -43,7 +45,7 @@ def main(): id_ = urllib.parse.quote_plus(id_) machine = {"id": id_, "name": name, "properties": properties} - with open(".benchmarks/machine.json", "w") as f: + with open(".benchmarks/machine.json", "w", encoding="utf-8") as f: json.dump(machine, f, indent=2, ensure_ascii=False) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index e263f4a12..1b6116a13 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -1,3 +1,4 @@ +"""Measurement script for the progress tracker""" import argparse import json import os @@ -99,7 +100,7 @@ def identify_metrics(script, lines, metrics): def create_modified_script(script_without_extension, lines, metrics): """Create a modified version of the script which can be used to perform measurements""" - with open(f"{script_without_extension}.measure.py", "w") as f: + with open(f"{script_without_extension}.measure.py", "w", encoding="utf-8") as f: # Import must-have libraries f.write("import json\n") f.write("import time\n") @@ -181,6 +182,7 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s process = subprocess.run( ["python", f"{script_without_extension}.measure.py"], capture_output=True, + check=True, ) # Print sample information @@ -203,7 +205,7 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s break # Read the measurements and delete the temporary file - with open(f"{script_without_extension}.measurements") as f: + with open(f"{script_without_extension}.measurements", encoding="utf-8") as f: results = json.load(f) os.unlink(f"{script_without_extension}.measurements") @@ -237,6 +239,7 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s def main(): + """Measurement script for the progress tracker""" parser = argparse.ArgumentParser(description="Measurement script for the progress tracker") parser.add_argument("base", type=str, help="directory which contains the benchmarks") @@ -248,7 +251,7 @@ def main(): base = pathlib.Path(args.base) samples = args.samples - with open(".benchmarks/machine.json", "r") as f: + with open(".benchmarks/machine.json", "r", encoding="utf-8") as f: machine = json.load(f) result = {"machine": machine, "metrics": {}, "targets": {}} @@ -257,7 +260,7 @@ def main(): # Process each script under the base directory for script in filter(lambda script: not str(scripts[0]).endswith("measure.py"), scripts): # Read the script line by line - with open(script, "r") as f: + with open(script, "r", encoding="utf-8") as f: lines = f.readlines() # Find the first non-empty line @@ -309,7 +312,7 @@ def main(): perform_measurements(script, script_without_extension, target_id, metrics, samples, result) # Dump the latest results to the output file - with open(".benchmarks/findings.json", "w") as f: + with open(".benchmarks/findings.json", "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) # Delete the modified script if the user doesn't care From 696e460c27aa653c1f40a4a145b6e1ecebcdf9cc Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 15 Sep 2021 20:06:21 +0200 Subject: [PATCH 0258/1104] fix: pylint. --- script/actions_utils/coverage_report_format.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/actions_utils/coverage_report_format.py b/script/actions_utils/coverage_report_format.py index a0609b876..b27f0149f 100755 --- a/script/actions_utils/coverage_report_format.py +++ b/script/actions_utils/coverage_report_format.py @@ -39,7 +39,10 @@ if __name__ == "__main__": parser.add_argument("--diff-cover-output", type=str, required=True) cli_args = parser.parse_args() + + # pylint: disable=broad-except try: main(cli_args) except Exception: traceback.print_exc() + # pylint: enable=broad-except From e45916fc5031395eda67074373349614264ecc4a Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 16 Sep 2021 09:49:16 +0200 Subject: [PATCH 0259/1104] chore: add missing +x flag on some shell files --- docker/release_resources/entry_point.sh | 0 script/source_format/format_python.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/release_resources/entry_point.sh mode change 100644 => 100755 script/source_format/format_python.sh diff --git a/docker/release_resources/entry_point.sh b/docker/release_resources/entry_point.sh old mode 100644 new mode 100755 diff --git a/script/source_format/format_python.sh b/script/source_format/format_python.sh old mode 100644 new mode 100755 From 157dcc44bcdf3485f26eddcfa9a7b729e9f68aae Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 16 Sep 2021 09:46:42 +0200 Subject: [PATCH 0260/1104] tools: add a script to easily upgrade python dependencies --- Makefile | 4 ++++ script/make_utils/upgrade_deps.sh | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 script/make_utils/upgrade_deps.sh diff --git a/Makefile b/Makefile index d0455f3e8..54db0939b 100644 --- a/Makefile +++ b/Makefile @@ -191,3 +191,7 @@ jupyter: release_docker: ./docker/build_release_image.sh .PHONY: release_docker + +upgrade_py_deps: + ./script/make_utils/upgrade_deps.sh +.PHONY: upgrade_py_deps diff --git a/script/make_utils/upgrade_deps.sh b/script/make_utils/upgrade_deps.sh new file mode 100755 index 000000000..e5b40e8bc --- /dev/null +++ b/script/make_utils/upgrade_deps.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# verbose output please +set -v + +no_dev_file=$(mktemp --suffix=.txt) +all_file=$(mktemp --suffix=.txt) +dev_file=$(mktemp --suffix=.txt) + +poetry show -o -t --no-dev | grep -v -e "--" | cut -d " " -f 1 | sed 's/$/\@latest/g' > "${no_dev_file}" +poetry show -o -t | grep -v -e "--" | cut -d " " -f 1 | sed 's/$/\@latest/g' > "${all_file}" +join -v1 -v2 "${all_file}" "${no_dev_file}" > "${dev_file}" +cat "${no_dev_file}" | xargs poetry add +cat "${dev_file}" | xargs poetry add --dev + +rm "${no_dev_file}" +rm "${dev_file}" +rm "${all_file}" From b92a70768df1edaf3597fcd4dafac8ae3c965c30 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 15 Sep 2021 12:06:18 +0300 Subject: [PATCH 0261/1104] doc: write arithmetic operations tutorial --- docs/user/tutorial/ARITHMETIC_OPERATIONS.md | 214 +++++++++++++++++++- 1 file changed, 213 insertions(+), 1 deletion(-) diff --git a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md index 710b9fbf7..f9368ff60 100644 --- a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md +++ b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md @@ -1,3 +1,215 @@ # Arithmetic Operations -Umut to do: #312 +In this tutorial, we are going to go over all arithmetic operations available in `concretefhe`. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. + +## Addition + +### Static ClearScalar and EncryptedScalar + +```python +def f(x): + return x + 42 +``` + +or + +```python +def f(x): + return 42 + x +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(3) == 45 +engine.run(0) == 42 +``` + +### Dynamic ClearScalar and EncryptedScalar + +```python +def f(x, y): + return x + y +``` + +or + +```python +def f(x, y): + return y + x +``` + +results in + +```python +engine.run(6, 4) == 10 +engine.run(1, 1) == 2 +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` +- `y = ClearScalar(UnsignedInteger(bits))` + +### EncryptedScalar and EncryptedScalar + +```python +def f(x, y): + return x + y +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` +- `y = EncryptedScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(7, 7) == 14 +engine.run(3, 4) == 7 +``` + +## Subtraction + +### Static ClearScalar and EncryptedScalar + +```python +def f(x): + return 3 - x +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(2) == 1 +engine.run(3) == 0 +``` + +### Dynamic ClearScalar and EncryptedScalar + +```python +def f(x, y): + return y - x +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` +- `y = ClearScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(2, 4) == 2 +engine.run(1, 7) == 6 +``` + +## Multiplication + +### Static ClearScalar and EncryptedScalar + +```python +def f(x): + return x * 2 +``` + +or + +```python +def f(x): + return 2 * x +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(2) == 4 +engine.run(5) == 10 +``` + +### Dynamic ClearScalar and EncryptedScalar + +```python +def f(x, y): + return x * y +``` + +or + +```python +def f(x, y): + return y * x +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` +- `y = ClearScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(2, 3) == 6 +engine.run(1, 7) == 7 +``` + +## Dot Product + +### Dynamic ClearTensor and EncryptedTensor + +```python +def f(x, y): + return np.dot(x, y) +``` + +or + +```python +def f(x, y): + return np.dot(y, x) +``` + +where + +- `x = EncryptedTensor(UnsignedInteger(bits), shape=(2,))` +- `y = ClearTensor(UnsignedInteger(bits), shape=(2,))` + +results in + +```python +engine.run([1, 1], [2, 3]) == 5 +engine.run([2, 3], [2, 3]) == 13 +``` + +## Combining all together + +```python +def f(x, y, z): + return 100 - (2 * (np.dot(x, y) + z)) +``` + +where + +- `x = EncryptedTensor(UnsignedInteger(bits), shape=(2,))` +- `y = ClearTensor(UnsignedInteger(bits), shape=(2,))` +- `z = EncryptedScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run([1, 2], [4, 3], 10) == 60 +engine.run([2, 3], [3, 2], 5) == 66 +``` From 7088f234cb2b96735ec4105918e4c114ab463c51 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 16 Sep 2021 11:51:42 +0300 Subject: [PATCH 0262/1104] chore: set notebook timeout to 3 hours --- Makefile | 16 ++++++------ .../QuantizedLinearRegression.ipynb | 6 ++++- .../QuantizedLogisticRegression.ipynb | 6 ++++- ...ebook_sanitize.py => notebook_finalize.py} | 26 +++++++++++++++---- 4 files changed, 39 insertions(+), 15 deletions(-) rename script/nbmake_utils/{notebook_sanitize.py => notebook_finalize.py} (57%) diff --git a/Makefile b/Makefile index 54db0939b..197cca3ee 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,9 @@ check_python_format: --dir $(SRC_DIR) --dir tests --dir benchmarks --dir script --check .PHONY: check_python_format -check_strip_nb: - poetry run python ./script/nbmake_utils/notebook_sanitize.py $(NOTEBOOKS_DIR) --check -.PHONY: check_strip_nb +check_finalize_nb: + poetry run python ./script/nbmake_utils/notebook_finalize.py $(NOTEBOOKS_DIR) --check +.PHONY: check_finalize_nb pylint: $(MAKE) --keep-going pylint_src pylint_tests pylint_benchmarks pylint_script @@ -62,7 +62,7 @@ flake8: python_linting: pylint flake8 .PHONY: python_linting -conformance: strip_nb python_format +conformance: finalize_nb python_format .PHONY: conformance pcc: @@ -70,7 +70,7 @@ pcc: --no-print-directory pcc_internal .PHONY: pcc -pcc_internal: check_python_format check_strip_nb python_linting mypy_ci pydocstyle +pcc_internal: check_python_format check_finalize_nb python_linting mypy_ci pydocstyle .PHONY: pcc_internal pytest: @@ -172,9 +172,9 @@ pydocstyle: poetry run pydocstyle $(SRC_DIR) --convention google --add-ignore=D1,D202 --add-select=D401 .PHONY: pydocstyle -strip_nb: - poetry run python ./script/nbmake_utils/notebook_sanitize.py $(NOTEBOOKS_DIR) -.PHONY: strip_nb +finalize_nb: + poetry run python ./script/nbmake_utils/notebook_finalize.py $(NOTEBOOKS_DIR) +.PHONY: finalize_nb pytest_nb: poetry run pytest --nbmake $(NOTEBOOKS_DIR)/*.ipynb diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index 1697a0d9f..163372c3f 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -735,7 +735,11 @@ "metadata": {} } ], - "metadata": {}, + "metadata": { + "execution": { + "timeout": 10800 + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index 3ea336579..b76c5e447 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -879,7 +879,11 @@ "metadata": {} } ], - "metadata": {}, + "metadata": { + "execution": { + "timeout": 10800 + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/script/nbmake_utils/notebook_sanitize.py b/script/nbmake_utils/notebook_finalize.py similarity index 57% rename from script/nbmake_utils/notebook_sanitize.py rename to script/nbmake_utils/notebook_finalize.py index c26ee33f5..81ba002a1 100644 --- a/script/nbmake_utils/notebook_sanitize.py +++ b/script/nbmake_utils/notebook_finalize.py @@ -1,11 +1,12 @@ -"""Sanitizer for Jupyter notebooks.""" +"""Finalizer for Jupyter notebooks.""" import argparse import json from pathlib import Path def main(): - """Sanitize""" + """Finalize""" + parser = argparse.ArgumentParser(description="Sanitizer for Jupyter Notebooks") parser.add_argument("base", type=str, help="directory which contains the notebooks") @@ -21,11 +22,26 @@ def main(): content = json.load(f) if args.check: - if len(content["metadata"]) != 0: + try: + metadata = content["metadata"] + assert len(metadata) == 1 + assert "execution" in metadata + + execution = metadata["execution"] + assert len(execution) == 1 + assert "timeout" in execution + + timeout = execution["timeout"] + assert timeout == 10800 # 3 hours + except Exception: print("Notebooks are not sanitized. Please run `make conformance`.") - raise ValueError + raise else: - content["metadata"] = {} + content["metadata"] = { + "execution": { + "timeout": 10800, # 3 hours + } + } with open(notebook, "w", newline="\n", encoding="utf-8") as f: json.dump(content, f, indent=1, ensure_ascii=False) f.write("\n") From 699d02a93bccdb065f4f7251611f5f2255e40447 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 16 Sep 2021 11:22:43 +0200 Subject: [PATCH 0263/1104] docs: add information in REDUCE_NEEDED_PRECISION.md --- docs/user/howto/REDUCE_NEEDED_PRECISION.md | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/user/howto/REDUCE_NEEDED_PRECISION.md b/docs/user/howto/REDUCE_NEEDED_PRECISION.md index 367a4360b..3c2d505fe 100644 --- a/docs/user/howto/REDUCE_NEEDED_PRECISION.md +++ b/docs/user/howto/REDUCE_NEEDED_PRECISION.md @@ -1,3 +1,25 @@ # Having a Function Which Requires Less Precision -Arthur to do: #319 +## Why can some computation work with less precision? + +### The input data uses more bits than required + +For some tasks, like classification for example, the output prediction often carries much less information than the input data used to make that prediction. + +For example the MNIST classification task consists in taking an image, a 28x28 array containing uint8 values, representing a number and predict whether it belongs to one of 10 classes: the numbers from 0 to 9 included. The output is a one-hot vector to indicate which class a particular sample belongs to. + +The input contains 28x28x8 = 6272 bits of information. In practice you could also get good results on MNIST by binarizing the images and training a model for that Binarized MNIST task. This means that in a real use case where you actually need to do digits recognition, you could binarize your input on the fly, replacing each pixel by either 0 or 1. Doing so, you use 1 bit per pixel and now only have 768 bits of input data. It also means that if you are doing some accumulation (adding pixel values together), you are going to need accumulators that are smaller (adding 0s and 1s requires less space than adding values ranging from 0 to 255 included). + +This shows how adapting your data can allow you to use models that may require smaller data types (i.e. use less precision) to perform their computations. + +```{note} +Binarizing here is akin to quantization which is introduced [here](../explanation/QUANTIZATION.md). You can also find further resources on the linked page. +``` + +### There is a tolerance on the result + +If for some reason you have a tolerance on the result's precision, then you can change the computation used to a certain extent and still be in that tolerance range. + +This is illustrated in both advanced examples [Quantized Linear Regression](../advanced_examples/QuantizedLinearRegression.ipynb) and [Quantized Logistic Regression](../advanced_examples/QuantizedLogisticRegression.ipynb). + +The end result has a granularity/imprecision linked to the data types used and for the Quantized Logistic Regression to the lattice used to evaluate the logistic model. From 15aeb355295340c3eeb9bcca4a80154a1bbc7280 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 16 Sep 2021 19:28:14 +0200 Subject: [PATCH 0264/1104] fix: dot tests were using a single-element inputset closes #372 --- tests/numpy/test_compile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 1036b0b91..fe7da5ca7 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -148,15 +148,15 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): ), pytest.param( 4, - (0, 8), + (0, 5), ), pytest.param( 8, - (0, 8), + (0, 4), ), pytest.param( 16, - (0, 4), + (0, 3), ), ], ) @@ -164,9 +164,11 @@ def test_compile_and_run_dot_correctness(size, input_range): """Test correctness of results when running a compiled function""" def data_gen(input_range, size): - for i in range(*input_range, size): - vec = list(range(i, min(i + size, input_range[1]))) - yield vec, vec[::-1] + for _ in range(1000): + low, high = input_range + args = [[random.randint(low, high) for _ in range(size)] for __ in range(2)] + + yield args function_parameters = { "x": EncryptedTensor(Integer(64, False), (size,)), From a7d0b87408aca4546d2165c95bcb8bbf8b15cfb5 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 17 Sep 2021 10:30:40 +0300 Subject: [PATCH 0265/1104] fix: revert check argument of subprocess.run as the return code is already checked below --- script/progress_tracker_utils/measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 1b6116a13..fe6fad3a6 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -182,7 +182,7 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s process = subprocess.run( ["python", f"{script_without_extension}.measure.py"], capture_output=True, - check=True, + check=False, ) # Print sample information From e2804c530026a985150c6df11a2fa09e6a864b2c Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 16 Sep 2021 09:39:17 +0200 Subject: [PATCH 0266/1104] fix: fix unclosed file in draw_graph closes #280 --- concrete/common/compilation/artifacts.py | 4 ++-- concrete/common/debugging/drawing.py | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index f35347dd1..eaefd8f3f 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -175,9 +175,9 @@ class CompilationArtifacts: f.write(f"{name} :: {parameter}\n") drawings = self.drawings_of_operation_graphs.items() - for index, (name, drawing) in enumerate(drawings): + for index, (name, drawing_filename) in enumerate(drawings): identifier = CompilationArtifacts._identifier(index, name) - drawing.save(output_directory.joinpath(f"{identifier}.png")) + shutil.copy(drawing_filename, output_directory.joinpath(f"{identifier}.png")) textual_representations = self.textual_representations_of_operation_graphs.items() for index, (name, representation) in enumerate(textual_representations): diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index bcca75469..305b75ed7 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -43,19 +43,18 @@ def draw_graph( show: bool = False, vertical: bool = True, save_to: Optional[Path] = None, -) -> Image.Image: +) -> str: """Draws operation graphs and optionally saves/shows the drawing. Args: opgraph (OPGraph): the graph to be drawn and optionally saved/shown show (bool): if set to True, the drawing will be shown using matplotlib vertical (bool): if set to True, the orientation will be vertical - save_to (Optional[Path]): if specified, the drawn graph will be saved to this path + save_to (Optional[Path]): if specified, the drawn graph will be saved to this path; else + it is saved in a temporary file Returns: - Pillow Image of the drawn graph. - This is useful because you can use the drawing however you like. - (check https://pillow.readthedocs.io/en/stable/reference/Image.html for further information) + The path of the file where the drawn graph is saved """ @@ -90,19 +89,21 @@ def draw_graph( agraph.layout("dot") if save_to is None: - with tempfile.NamedTemporaryFile(suffix=".png") as tmp: - agraph.draw(tmp.name) - img = Image.open(tmp.name) + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + save_to_str = str(tmp.name) else: - agraph.draw(save_to) - img = Image.open(save_to) + save_to_str = str(save_to) + + agraph.draw(save_to_str) if show: # pragma: no cover # We can't have coverage in this branch as `plt.show()` blocks and waits for user action. plt.close("all") plt.figure() + img = Image.open(save_to_str) plt.imshow(img) + img.close() plt.axis("off") plt.show() - return img + return save_to_str From e5ea30b68936887d59e31da06ed670ae68ec6616 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 16 Sep 2021 12:06:34 +0200 Subject: [PATCH 0267/1104] test: and now, we can remove an ignore closes #280 --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dc304f9ab..36c5ad7f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,8 +44,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] filterwarnings = [ - "error", - "ignore::UserWarning", + "error" ] From 25d40a43489869d796cf1ae267bf897643965603 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 16 Sep 2021 17:01:30 +0200 Subject: [PATCH 0268/1104] fix: and fix notebooks after change in draw_graph closes #280 --- .../QuantizedLinearRegression.ipynb | 387 ++++++++------ .../QuantizedLogisticRegression.ipynb | 498 ++++++++++-------- 2 files changed, 480 insertions(+), 405 deletions(-) diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index 163372c3f..56adac9ca 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -2,115 +2,129 @@ "cells": [ { "cell_type": "markdown", + "id": "b760a0f6", + "metadata": {}, "source": [ "# Quantized Linear Regression\n", "\n", "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "253288cf", + "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our linear regression model" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 1, + "id": "6200ab62", + "metadata": {}, + "outputs": [], "source": [ "import numpy as np" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "f43e2387", + "metadata": {}, "source": [ "### And some helpers for visualization" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 2, + "id": "d104c8df", + "metadata": {}, + "outputs": [], "source": [ "%matplotlib inline\n", "\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "53e676b8", + "metadata": {}, "source": [ "### We need an inputset, a handcrafted one for simplicity" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 3, + "id": "d451e829", + "metadata": {}, + "outputs": [], "source": [ "x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", "y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "75f4fdb7", + "metadata": {}, "source": [ "### Let's visualize our inputset to get a grasp of it" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 4, + "id": "2a124a62", + "metadata": {}, + "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 5, - "source": [ - "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", - "display(fig)" - ], + "id": "edcd361b", + "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "image/png": "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", + "image/png": "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\n", "text/plain": [ "
" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], - "metadata": {} + "source": [ + "ax.scatter(x[:, 0], y, marker=\"x\", color=\"red\")\n", + "display(fig)" + ] }, { "cell_type": "markdown", + "id": "5c8310ab", + "metadata": {}, "source": [ "### Now, we need a model so let's define it\n", "\n", "The main purpose of this tutorial is not to train a linear regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 6, + "id": "91d4a1da", + "metadata": {}, + "outputs": [], "source": [ "class Model:\n", " w = None\n", @@ -132,145 +146,160 @@ "\n", " def evaluate(self, x):\n", " return x @ self.w + self.b" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "faa5247c", + "metadata": {}, "source": [ "### And create one" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 7, + "id": "682fb2d8", + "metadata": {}, + "outputs": [], "source": [ "model = Model().fit(x, y)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "084fb296", + "metadata": {}, "source": [ "### Time to make some predictions" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 8, + "id": "4953b03e", + "metadata": {}, + "outputs": [], "source": [ "inputs = np.linspace(40, 210, 100).reshape(-1, 1)\n", "predictions = model.evaluate(inputs)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "f28155cf", + "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 9, - "source": [ - "ax.plot(inputs, predictions, color=\"blue\")\n", - "display(fig)" - ], + "id": "111574ed", + "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAD4CAYAAAAXUaZHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAhT0lEQVR4nO3deXxU1fnH8c+jWCqKxgUtgopWrbKDUUFFrbjhbheq7a+igoggDVErahej1Yq4xGgRBUHBlSIIiKyySmUL+yayCAoqoLIoKBJyfn+cOzoJCUlIJvdm5vt+vfLKnTN3ksd5jQ9PnnvuOeacQ0REkst+YQcgIiIVT8ldRCQJKbmLiCQhJXcRkSSk5C4ikoSqhR0AwJFHHunq1asXdhgiIlXKnDlzvnTO1SrquUgk93r16pGbmxt2GCIiVYqZrS3uObVlRESSkJK7iEgSUnIXEUlCSu4iIklIyV1EJAkpuYuIJCEldxGRJKTkLiISgh07oHt3WFvsTPXyUXIXEalkEydCo0bQsyeMGpWY36HkLiJSSbZsgVtvhdatYb/9YPJkuP32xPwuJXcRkUowYgQ0aAD9+8Pdd8OCBXD++Yn7fUruIiIJtHEjXH89XHMNHHEEzJwJjz8ONWok9vcquYuIJIBz8OqrcNppMHQoPPQQ5OZCenrl/P5IrAopIpJMPv0UOnXyF0tbtIB+/aB+/cqNQZW7iEgFyc+H3r19b33yZMjOhmnTKj+xgyp3EZEKsWIFdOgAU6f62TB9+sCJJ4YXjyp3EZFyyMvz89UbN/YzYPr1g/Hjw03soMpdRGSfLVgA7dvDnDlw7bXQqxccc0zYUXmq3EVEymjnTvjHP/zMl08/hcGD/YyYqCR2UOUuIlIm06f7an3ZMrjxRnjqKT9/PWpUuYuIlMK330K3bnDOObB9O4weDQMGRDOxgyp3EZESjR8PHTvCmjXQpbPj0R5GzZrBk86BWZjhFUmVu4hIMTZv9i2YSy6B6tXh/Zv7858DMql5sPMnOAeZmZCVFWqcRVFyFxEpwrBh/uajAQPgvvtg/jzHuYcshJwcn9BjiT0nxy/36FzYIRegtoyISJwNG6BrVz8DpkkTePddaN4cwPwtp+ATek6OP87I8OMRa82ochcRwRfeAwf6hb5GjIBHHoHZs2OJPWBxCT4mgokdlNxFRFi7Ftq0gXbtfHKfPx/uvx8OOKDQibFWTLxYiyZilNxFJGXl5/u7Shs29At8PfssvP8+nHpqESfH99gzMvyLMzIK9uAjRD13EUlJy5f7hb6mTfOzYfr0geOP38sLzCAtrWCPPdaiSUuLXGvGXAT+tUlPT3e5ublhhyEiVVXhueZ7mXu+axc88QQ8+KDfDSk7299pWurcXIbflWhmNsc5V+T2H6Vqy5jZGjNbZGbzzSw3GDvczMab2Yrg+2HBuJnZM2a20swWmlnzvf90EZFyyMoq2BbZy9zzefPgrLN8P/3KK2HpUt9nL1NuLnxyxCr2mLL03H/tnGsa96/EvcAE59zJwITgMUAb4OTgqyPQu6KCFREpwDk/x7yEuefff+8T+hlnwGefwZAh8NZb8ItfhBp9QpWn534NcEFwPACYDHQPxgc63++ZYWZpZlbbOfd5eQIVEdlDfN+7mLnn06b53vry5XDTTX6hr8MOCy3iSlPayt0B48xsjpl1DMaOjkvYXwBHB8d1gE/jXrsuGCvAzDqaWa6Z5W7atGkfQhcRodi55998a3TtCued5yv3cePgpZdSI7FD6ZP7uc655viWSxczOy/+yaBKL9OVWedcH+dcunMuvVatWmV5qYjIT4qYez72Ny/QsKGjVy9/t+nixXDxxSHFF5JSJXfn3Prg+0bgbeBMYIOZ1QYIvm8MTl8PHBv38rrBmIhIxSo09/zrL/O56bSZXDasEzW+2cj7Ux05OXDwwWEHWvlKTO5mdpCZ1YwdA5cAi4ERQLvgtHbA8OB4BHBjMGumBbBV/XYRSYi4uedvnZPNafWNVz86g7+dMY55nftyzrnRnMlSGUpzQfVo4G3z032qAa8758aY2Wzgv2bWHlgLtA3OHwVcDqwEdgA3V3jUIiKBLzpl0aWLY2hbo1kzGDvWaNrkYrBLwg4tVCUmd+fcaqBJEeNfAa2LGHdAlwqJTkSkGM755XgzM+G774wePeCuu6BaNYDUrdhjtPyAiFQ5a9b4nZHGj4dWreDFF+GUU8KOKlq0cJiIVBm7d8Mzz/iFvqZP94t+TZ6sxF4UVe4iUiUsW+ZvRvrgA7887/PPw3HHhR1VdKlyF5FI27XLb5zRtCl8+CG88orfHUmJfe9UuYtIZM2ZA7fcAgsXQtu2fr31o44KO6qqQZW7iETOd9/Bvff6FRw3bfKbVQ8apMReFqrcRSRSpk71vfUVK/z3xx/39ylJ2ahyF5FI2LYNOneG88+HvDx47z3o21eJfV8puYtI6EaN8tMbn38eunWDRYug9R63SEpZqC0jIqH56iufzF99FerX99McW7QIO6rkoMpdRCqdc/Df/8Jpp8Gbb8I//gFz5yqxVyRV7iJSqT77zPfWhw+H9HTfW2/cOOyoko8qdxGpFM5Bv36+/TJ2LPTs6ZcQUGJPDFXuIpJwq1f7hb4mTPCzYV58EU46KeyokpsqdxFJmN274emnoVEjmDXLz4aZOFGJvTKocheRhFiyBNq3h5kz4YorfGKvWzfsqFKHKncRqVA//AAPPQTNmsGqVfD66/DOO0rslU2Vu4hUmNmzfbW+aBFcf71fe71WrbCjSk2q3EWk3HbsgL/+1c9T//prGDEC3nhDiT1MqtxFpFwmT/YLfK1aBbfe6hf6OvTQsKMSVe4isk+2boXbboNf/9o/njgR+vRRYo8KJXcRKbORI6FBAz9f/a67/GYasSQv0aDkLiKltmkT/PGPcNVVcNhh/g7TJ56AGjXCjkwKU3IXkRI55y+Q1q8Pb70FWVl+C7wzzww7MimOLqiKyF6tW+cX+nrnHZ/M+/Xza69LtKlyF5GCnAMgP99fIG3QwPHee/Dkk369dSX2qkHJXUR+kpUFmZmsXOFo3drPhjn9kBUs6pDDnXfC/vuHHaCUlpK7iHjOkff1Np7IqUaj03Yxd66jT+s3mbDuV/xyv49/rOilalDPXUQAWLTYaD/jSWZjXL17OM9t60ydCZ9BRgZkZ4NZ2CFKGahyF0lxO3fCAw9A8+awZo3x5huOYVxLHT7zJyixV0lK7iIpbOZMOP10v4rj9dfD0iWOP8zIpEAqz8xUS6YKUnIXSUHbt8Odd0LLln4ZgXffhVcGOo58JBNycnwrJj/ff8/JUYKvgtRzF0kxEyb4Bb4+/tjPX3/0UTjkEACDtLSCPfbsbP+itDS1ZqoYJXeRFLFlC9x9t78J6eSTYcoUOO+8QidlZfkKPZbIYwleib3KUVtGpCoo3BIpY4tk+HC/dMDLL0P37rBgQRGJPaZwIldir5JKndzNbH8zm2dmI4PHJ5jZTDNbaWaDzOxnwXj14PHK4Pl6CYpdJDUENxb9mNCd84+zskp86caN8Ic/wLXX+o0zZs6EHj3gwAMTGbBEQVkq9wxgWdzjx4Bs59xJwGagfTDeHtgcjGcH54nIvnDO91PiL2pmBhc9t2wptoJ3Dl57zVfrw4bBww9Dbq6fGSMpwjlX4hdQF5gAXAiMBAz4EqgWPN8SGBscjwVaBsfVgvNsbz//9NNPdyJSjPx85zIynPM5239lZPjxInzyiXOXX+5Pa9HCuSVLKjVaqURArismr5a2cn8auAfIDx4fAWxxzuUFj9cBdYLjOsCnwT8cecDW4PwCzKyjmeWaWe6mTZtKGYZICoqftRJTxEXO/Hzo3dtvojF5si/up03z1buknhKTu5ldCWx0zs2pyF/snOvjnEt3zqXX0i66IsWLtWLiFZp3/tFHcMEFfmrjWWfB4sXwl79ooa9UVprK/RzgajNbA7yJb83kAGlmFptKWRdYHxyvB44FCJ4/FPiqAmMWSR3xPfYibizK2+Xo2ROaNIFFi6B/fxg3Dk44IezAJWwlznN3zt0H3AdgZhcAdzvn/mRmg4Hf4RN+O2B48JIRwePpwfMTg96QiJSVFX9j0YLvf8UtLYy5c+G666BXL6hdO9xwJTrKcxNTd+BNM3sYmAf0C8b7Aa+Y2Urga+D68oUokuIK3Vi08wfj4YOz6dHLOOIIv+3db38bbogSPWVK7s65ycDk4Hg1sMcOis6574HfV0BsIhITJPYPPoD27eHDD40bb/RF/OGHhxybRJLuUBWpAr791ndmzj0XduyAMWNgwAAldime1pYRibhx46BjR/jkk58W+qpZM+yoJOpUuYtE1ObNcMstcOml8POfw9Sp8J//KLFL6Si5i0TQ0KH+5qOBA+G++2D+fN+SESkttWVEIuSLL+COO2DIEGjaFEaNgmbNwo5KqiJV7iIR4Jy/QFq/PowcCY88ArNmKbHLvlPlLhKytWuhUyc/A+bss/1mGqeeGnZUUtWpchcJSX6+v6u0YUN4/3145hn/XYldKoIqd5EQLF8OHTr4VRsvvRReeAGOPz7sqCSZqHIXqUS7dvl56k2awJIlftu70aOV2KXiqXIXqSTz5vmlA+bNg9/9Dp59Fn7xi7CjkmSlyl0kwb7/Hu6/H844Az7/3E9zHDxYiV0SS5W7SAJNm+Z768uXw803w5NPwmGHhR2VpAJV7iIJ8M03/makVq185T5unN9IQ4ldKouSu0gFGzvWT2987jm/kuPixXDxxWFHJalGyV2kgnz1FbRrB5ddBjVq+JbM00/DwQeHHZmkIiV3kXJyzu+GVL8+vP46/P3vfqGvs88OOzJJZbqgKlIOn38OXbrA229D8+a+t96kSdhRiahyF9knzsFLL/lqffRoeOwxmDlTiV2iQ5W7SBmtWeN3Rho/3s+GefFFOOWUsKMSKUiVu0gp7d7tF/dq2BCmT/ezYSZPVmKXaFLlLlIKy5b5pQOmT4c2beD55+G448KOSqR4qtxF9mLXLr9xRtOm/i7TV16Bd98NErtzBU8u/FgkREruIsWYMwfS0/3Uxmuv9dX7//0fmAFZWZCZ+VNCd84/zsoKL2CROEruIoV89x107w5nnQWbNvlpjoMGwVFHBSc4B1u2QE7OTwk+M9M/3rJFFbxEgnruInGmTIFbb4UVK3yP/YknIC2t0ElmkJ3tj3Ny/Bf4tQays4PSXiRcqtxFgG3b4Pbb4YILIC8P3nvPT3HcI7HHxCf4GCV2iRAld0l5o0ZBgwZ+q7tu3WDRImjduoQXxVox8eJ78CIhU3KXlPXll/4C6RVXwCGH+GmO2dlw0EElvDC+x56R4Xe6zsgo2IMXCZl67pJynPMXSLt29dc///lPv1NS9eql/AFmvl8T32OPtWjS0tSakUgwF4EqIz093eXm5oYdhqSA9euhc2cYMcJPc+zfHxo12scf5lzBRF74sUiCmdkc51x6Uc+pLSMpwTno29cv9DVuHPTs6dsw+5zYYc9ErsQuEaK2jCS91av99MaJE+H88/0smJNOCjsqkcRS5S5Ja/du3wpv2BByc/16MBMnKrFLaigxuZvZz81slpktMLMlZvZgMH6Cmc00s5VmNsjMfhaMVw8erwyer5fg/waRPSxeDOecA3fe6ac1LlkCt90G+6mckRRRmo/6TuBC51wToClwmZm1AB4Dsp1zJwGbgfbB+e2BzcF4dnCeSKX44Qd48EG/K9KqVX7buxEjoG7dsCMTqVwlJnfnfRs8PCD4csCFwFvB+ADg2uD4muAxwfOtzXSlSSrIXlZinD0bTj/dr931+9/D0qVwww26zimpqVQXVM1sf2AOcBLQC1gFbHHO5QWnrAPqBMd1gE8BnHN5ZrYVOAL4stDP7Ah0BDhOC2NLaWRl+Ynpsbnlwc1EOw6qxQM//I2nnoLatX2lftVVYQcrEq5SJXfn3G6gqZmlAW8Dp5b3Fzvn+gB9wM9zL+/PkyQXvxIj+ASfmcnknPnceuhgVm71PfXHHoNDDw01UpFIKNNUSOfcFjObBLQE0sysWlC91wXWB6etB44F1plZNeBQ4KsKjFlSUaGVGLfmvMQ99KQPT/PLIx2ThvlFv0TEK81smVpBxY6ZHQhcDCwDJgG/C05rBwwPjkcEjwmen+iicBusVH1Bgh/JFTRgCS/SgbvvcixcaErsIoWUZrZMbWCSmS0EZgPjnXMjge7AnWa2Et9T7xec3w84Ihi/E7i34sOWVLRpo+OPp87lKkZyGJuZTksez8ukxoGqHUQKK7Et45xbCDQrYnw1cGYR498Dv6+Q6ETw7fY3Xnf8pcN2tn3fiIdajKL75Db8rPvZBXvwmhYj8iMtPyCRtm6d30Rj5EjjrDpb6PfrV2kwsLtWYhQpgZK7RFJ+vl8D5q9/9TsjZWdD16512X+/7j8l8liCV2IX2YOSu0TOypV+oa/Jk+HCC/1qjieeGHtWKzGKlIZW2pDIyMvzG1I3agTz5vnK/b334hO7iJSWKnepHCVsbLFwIbRv71dvvPpq6N0bjjkmhDhFkoQqd0m8rKyCe4vG9iDNymLnTnjgAb8mzNq1fvu7YcOU2EXKS8ldEit+2YBYgg82l56x7FCaN3c89JBf4GvZMmjbVm10kYqgtowkVqFlA8jJYTs1+EezSTw9+Hzq1jVGjYI2bcINUyTZqHKXxItL8BO4kEYsInveBXTqZCxerMQukghK7pJ4zrGl8/10oC8XMYFq5DHld8/wXC/HIYeEHZxIclJbRhLLOYZd3Z/OI7uywX7BPXc7snb05cBeT0Dmat2EJJIgSu6SMBs2QNeuxuCR7Wl85HpGjDLSzzBwPaHaLi0bIJJASu5S4ZyD116DjAz49lt4+GG456/HcMDPtGyASGVRcpcK9ckn0KkTjB4NLVv6u0zr1wctGyBSuXRBVSpEfj489xw0aABTp8Izz8D778cSu4hUNlXuUm4ffQQdOvhkftFFfqGvevXCjkoktalyl32Wl+c3pG7cGBYtgpdegnHjlNhFokCVu+yTBQvglltg7ly47jro1Qtq1w47KhGJUeUuZfL99/D3v0N6ut8lafBgGDpUiV0kalS5S6l98IHvrS9bBu3awVNPweGHhx2ViBRFlbuU6Ntv/Zz1c8+F7dthzBh4+WUldpEoU+UuezV+PHTs6Nda79IF/v1vqFkz7KhEpCSq3KVImzfDzTfDJZdA9ep+7vqzzyqxi1QVSu6yh6FD/c1Hr7wC990H8+f7loyIVB1qy8iPvvgC7rgDhgyBpk3h3XehefOwoxKRfaHKPVnF9ist7nGhpwYM8NX6yJHw6KMwa5YSu0hVpuSejPayIXVha9f6nZBuusmvC7NgAdx7LxxwQGUGLCIVTck92exlQ2q2bPkx4efnw3/+4xP6//7nj6dMgV/9KtToRaSCqOeebIrYkBrwE9WDNdSXL4f27X1Sv/RSeOEFOP748EIWkYqnyj0ZxSf4mOxsduUZjz4KTZrA0qW+zz56tBK7SDJSck9GsVZMnLl/epIzz3Tcfz9cdZVfQuDGG7VnhkiyUnJPNvE99owMvtuez33p4znzjW588dE2hrzlGDwYjj467EBFJJGU3JONmd94OiODab/Npmkzo0fuRbSrn8vSO3rzm9+qVBdJBbqgmoS+uSuL++519DrPqFfPrw9zUeuzwFqEHZqIVBJV7klmzBho2BCe62385S9+h6SLLkLNdZEUU2JyN7NjzWySmS01syVmlhGMH25m481sRfD9sGDczOwZM1tpZgvNTPc5VoKvvvJrrLdpAwcd5Kc55uTAwQeHHZmIhKE0lXsecJdzrj7QAuhiZvWBe4EJzrmTgQnBY4A2wMnBV0egd4VHLT9yzu+GVL8+vP663yVp3jxo2TLsyEQkTCUmd+fc5865ucHxN8AyoA5wDTAgOG0AcG1wfA0w0HkzgDQz0yZsCfDZZ/Cb30DbtnDssZCbC//6l1+iV0RSW5l67mZWD2gGzASOds59Hjz1BRCbXFcH+DTuZeuCscI/q6OZ5ZpZ7qZNm8oad0pzDvr189X6mDHQowfMmOFvThIRgTIkdzM7GBgCdHPObYt/zjnngOKXHSyCc66Pcy7dOZdeq1atsrw0pX38sd9Ao0MHaNzYL/TVvTtU07wnEYlTquRuZgfgE/trzrmhwfCGWLsl+L4xGF8PHBv38rrBmJTD7t3+AmnDhr5K79ULJk+GU04JOzIRiaLSzJYxoB+wzDn3VNxTI4B2wXE7YHjc+I3BrJkWwNa49o3sg6VLoVUr6NYNzj/fP+7cGfbTRFYRKUZp/pg/B/gzsMjM5gdj9wM9gP+aWXtgLdA2eG4UcDmwEtgB3FyRAaeSH36Anj39RdKaNf22d3/6k6asi0jJSkzuzrlpQHHppHUR5zugSznjSnm5uX5Z3oUL4frrfUvmqKPCjkpEqgr9YR8x330H99wDZ50FX34Jw4fDG28osYtI2WiORYRMmQK33gorVvjvPXv6NcBERMpKlXsEbNsGt98OF1zgZ8VMmAB9+iixi8i+U3IP2ahRfh/TPn3gzjv9Ql8XXhh2VCJS1Sm5h+TLL+HPf4YrroBDD4UPPoAnn4QaNcKOTESSgZJ7JXMOBg3ySwcMGgQPPABz5/oLqCIiFUUXVCvR+vX+5qMRI+CMM/z6MI0ahR2ViCQjVe6VwDno29dX6+PGwRNPwPTpSuwikjiq3BNs1So/rXHSJD8bpm9fOOmksKMSkWSnyj1Bdu+Gp57y1fmcOX42zMSJSuwiUjlUuSfA4sV+6YBZs+Cqq6B3b6izx4r2IiKJo8q9Av3wAzz4IDRvDqtX+23vhg9XYheRyqfKvYLMmuWr9cWL4YYb/EJf2oNERMKiyr2cduyAu+/2G1Jv3gzvvOMrdiV2EQmTKvdymDTJb3e3ejXcdhs89pi/21REJGyq3PfB1q0+mV94od84Y9IkeP55JXYRiQ4l9zJ65x1/M9KLL/p2zMKFfv66iEiUKLmX0qZN/kLp1VfDEUfAzJnw+ONa6EtEoknJvQTO+Qukp50GQ4b4qY65uZCeHnZkIiLF0wXVvfj0U7+Jxrvv+lUb+/Xza6+LiESdKvci5OfDCy/4RD5pEmRnw//+p8QuIlWHKvdCYvuXTpkCrVv7NWFOPDHsqEREykaVeyAvzy/F27gxzJ/vV28cP16JXUSqJlXu+OmM7dv7C6XXXAPPPQfHHBN2VCIi+y6lK/edO+Gf/4TTT4e1a+HNN+Htt5XYRaTqS9nKfcYMX60vXeo3qs7O9vPXcQ6wsMMTESmXlKvct2+HzEw4+2z4Zt1WRl3zAgMHuJ8Se2YmZGWFHaaISLmkVHKfMMHvjPT009DpNsfiP/6bNsM7+YQeS+w5ObBlS1DBi4hUTSnRltmyxa8D068fnHwyTJ0KrVoZuB5QfadP6Dk5/uSMDN+jMbVmRKTqSvrKfdgwv9DXyy9D9+6wYAG0ahU8aeYTeTwldhFJAkmb3DdsgLZt4brr4Kij/EJfPXrAgQfGnRRrxcSLtWhERKqwpEvuzsHAgX6hr+HD4ZFHYPZsP91xjxNjPfaMDL/mQEaGf6wELyJVXFL13D/5xG+iMWaMnw3z4os+yRfJDNLSCvbYYy2atDS1ZkSkSjMXgQo1PT3d5ebm7vPr8/P9Tkjdu/uC+9FHoUsX2K80f5c4VzCRF34sIhJRZjbHOVfkAuQlpj8z629mG81scdzY4WY23sxWBN8PC8bNzJ4xs5VmttDMmlfcf0bRli+H88/3ybxlS1i8GLp2LWVihz0TuRK7iCSB0qTAl4HLCo3dC0xwzp0MTAgeA7QBTg6+OgK9KybMovXvD02a+IT+0kswdizUq5fI3ygiUjWUmNydc1OBrwsNXwMMCI4HANfGjQ903gwgzcxqV1CsezjlFLjySli2DG66SUW3iEjMvl5QPdo593lw/AVwdHBcB/g07rx1wdjnFGJmHfHVPccdd9w+BXHuuf5LREQKKvdUSOevyJb5qqxzro9zLt05l16rVq3yhiEiInH2NblviLVbgu8bg/H1wLFx59UNxkREpBLta3IfAbQLjtsBw+PGbwxmzbQAtsa1b0REpJKU2HM3szeAC4AjzWwd8ADQA/ivmbUH1gJtg9NHAZcDK4EdwM0JiFlEREpQYnJ3zt1QzFOtizjXAV3KG5SIiJRP0q0tIyIiSu4iIklJyV1EJAlFYuEwM9uEvzAbpiOBL0OOoawUc+JVtXhBMVeWKMR8vHOuyBuFIpHco8DMcotbXS2qFHPiVbV4QTFXlqjHrLaMiEgSUnIXEUlCSu4/6RN2APtAMSdeVYsXFHNliXTM6rmLiCQhVe4iIklIyV1EJAmlbHI3szVmtsjM5ptZbjBW5N6wYTOzXwVxxr62mVk3M8sys/Vx45eHHGek99stQ8yPm9mHQVxvm1laMF7PzL6Le7+fj1DMxX4WzOy+4H1ebmaXRijmQXHxrjGz+cF46O+zmR1rZpPMbKmZLTGzjGA80p/nApxzKfkFrAGOLDTWE7g3OL4XeCzsOIuIe3/87lfHA1nA3WHHFBfbeUBzYHFJ7yl+9dDRgAEtgJkRivkSoFpw/FhczPXiz4vY+1zkZwGoDywAqgMnAKuA/aMQc6HnnwT+GZX3GagNNA+OawIfBe9lpD/P8V8pW7kXo7i9YaOkNbDKORf2Hb17cBHeb7c4RcXsnBvnnMsLHs7AbzoTGcW8z8W5BnjTObfTOfcxfjnuMxMWXDH2FrOZGX7Z8DcqNai9cM597pybGxx/AyzDbxka6c9zvFRO7g4YZ2Zzgv1cofi9YaPkegr+T3BH8Gdg/6i0kQop6367UXMLviKLOcHM5pnZFDNrFVZQxSjqs1AV3udWwAbn3Iq4sci8z2ZWD2gGzKQKfZ5TObmf65xrDrQBupjZefFPOv+3VqTmiZrZz4CrgcHBUG/gl0BT/CbkT4YTWelE8T3dGzP7G5AHvBYMfQ4c55xrBtwJvG5mh4QVXyFV6rNQyA0ULFgi8z6b2cHAEKCbc25b/HNR/zynbHJ3zq0Pvm8E3sb/qVrc3rBR0QaY65zbAOCc2+Cc2+2cywf6EsKf26VQJffbNbObgCuBPwX/ExO0Nr4Kjufg+9enhBZknL18FqL+PlcDfgMMio1F5X02swPwif0159zQYLjKfJ5TMrmb2UFmVjN2jL+Atpji94aNigIVTqGe3nX4/4aoqXL77ZrZZcA9wNXOuR1x47XMbP/g+ETgZGB1OFEWtJfPwgjgejOrbmYn4GOeVdnx7cVFwIfOuXWxgSi8z8F1gH7AMufcU3FPVZ3Pc9hXdMP4Ak7EzyBYACwB/haMHwFMAFYA7wGHhx1rXMwHAV8Bh8aNvQIsAhbiP1y1Q47xDfyf1LvwPcf2xb2n+FkFvfBV2SIgPUIxr8T3T+cHX88H5/42+LzMB+YCV0Uo5mI/C8Dfgvd5OdAmKjEH4y8DnQqdG/r7DJyLb7ksjPscXB71z3P8l5YfEBFJQinZlhERSXZK7iIiSUjJXUQkCSm5i4gkISV3EZEkpOQuIpKElNxFRJLQ/wMtV4lCm6lYAwAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], - "metadata": {} + "source": [ + "ax.plot(inputs, predictions, color=\"blue\")\n", + "display(fig)" + ] }, { "cell_type": "markdown", + "id": "23852861", + "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 10, - "source": [ - "print(model.w)\n", - "print(model.b)" - ], + "id": "7877cb2e", + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "[[2.669915]]\n", - "-3.2335143\n" + "-3.2335129\n" ] } ], - "metadata": {} + "source": [ + "print(model.w)\n", + "print(model.b)" + ] }, { "cell_type": "markdown", + "id": "de63118c", + "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "2d959640", + "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 11, - "source": [ - "from IPython.display import SVG\n", - "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ], + "id": "9da2e1a4", + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { - "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
", + "image/svg+xml": [ + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + ], "text/plain": [ "" ] }, + "execution_count": 11, "metadata": {}, - "execution_count": 11 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "from IPython.display import SVG\n", + "SVG(filename=\"figures/QuantizationVisualized.svg\")" + ] }, { "cell_type": "markdown", + "id": "45d12e7a", + "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 12, + "id": "2541cdb7", + "metadata": {}, + "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -403,59 +432,65 @@ " domain = np.array(range(2**input_bits), dtype=np.uint)\n", " table = f(domain).round().clip(0, 2**output_bits - 1).astype(np.uint)\n", " return QuantizedFunction(table)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "ab82ae87", + "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 13, + "id": "c8b08ef4", + "metadata": {}, + "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(model.w, parameter_bits)\n", "b_q = QuantizedArray.of(model.b, parameter_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "e2528092", + "metadata": {}, "source": [ "### And quantize our inputs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 14, + "id": "affe644e", + "metadata": {}, + "outputs": [], "source": [ "input_bits = 6\n", "\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "a5a50eb8", + "metadata": {}, "source": [ "### Time to make quantized inference" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 15, + "id": "0fdfd3d9", + "metadata": {}, + "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -464,48 +499,52 @@ "y_q = x_q.affine(w_q, b_q, min_y, max_y, output_bits)\n", "\n", "quantized_predictions = y_q.dequantize()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "5fb15eb4", + "metadata": {}, "source": [ "### And visualize the results" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 16, - "source": [ - "ax.plot(inputs, quantized_predictions, color=\"black\")\n", - "display(fig)" - ], + "id": "8076a406", + "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "image/png": "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", + "image/png": "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\n", "text/plain": [ "
" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], - "metadata": {} + "source": [ + "ax.plot(inputs, quantized_predictions, color=\"black\")\n", + "display(fig)" + ] }, { "cell_type": "markdown", + "id": "af6bc89e", + "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 17, + "id": "cbda8067", + "metadata": {}, + "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (max_y - min_y)\n", "zp_y = int(round(min_y * q_y))\n", @@ -521,12 +560,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b8e95e3d", + "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -543,28 +582,32 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "c6e101ae", + "metadata": {}, "source": [ "### Let's import the concrete numpy package now!" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 18, + "id": "4da7aed5", + "metadata": {}, + "outputs": [], "source": [ "import concrete.numpy as hnp" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 19, + "id": "d3816fa5", + "metadata": {}, + "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -580,20 +623,22 @@ "\n", "def infer(x_0):\n", " return table[(x_0 + zp_x) * w_0]" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "01d67c28", + "metadata": {}, "source": [ "### Let's compile our quantized inference function to it's operation graph for visualization" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 20, + "id": "81304aca", + "metadata": {}, + "outputs": [], "source": [ "inputset = []\n", "for x_i in x_q:\n", @@ -604,27 +649,25 @@ " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", " inputset,\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "c62af039", + "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 21, - "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" - ], + "id": "0c533af6", + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "%0 = Constant(1) # ClearScalar>\n", "%1 = x_0 # EncryptedScalar>\n", @@ -637,102 +680,104 @@ ] } ], - "metadata": {} + "source": [ + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ] }, { "cell_type": "code", "execution_count": 22, - "source": [ - "hnp.draw_graph(homomorphic_model).show()" - ], + "id": "c1fc0f48", + "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGCzZs306tXL5dN1sLCQnbs2FHr6zRu3Njhc0lJCbNnzw4mIQK6maqpgtjYWBYvXswzzzzjdMxd/cbLL7+8rDkcFhbGU089Rffu3Wv9vf6GFqOmSkJDQ3nrrbeYMWMGYWFhDrWkO8RosVho0KABAAkJCbz66qu1/k5/RA9taKrNQw89RJs2bejXrx95eXkUFxc7OHGOc5zt1n8HOMBRjnKc45zkJLnkUkopeeRRTDF1qEMkkUQRRRxxXGh0AX6F7rO7M6/OPFJIIZlk6hIc8W9AO3A0l8C+ffvoc3sfsvdkYwm1cHve7WyI3sApTl36l94BJALTjF0WLCSRRCqp9KAHaaSRSGJtzfdV9KoNTfUppJCVrOQLvmDhmYX8evevsBxYB3RzPj+EEBrRiIY0pAENCCWUWGIJI4wLXKCAAi5ykRxy+Pm1nyl4ogCqWJRxLdcykIHcxV2kkFL5yf6FFqOmavayl+lMZxaz+I3fjAMlwEQIaRJCh8c70IlOtKMdbWlLK1rRmMaEVbMnVFRURFF4EYc4xE52soMdbGc761nPEY64/JuOdGQsY7mXe4khxg13aipajJqK2cAGJjGJZSxDcHxMmtGMO7mTXvTi/x34f6Q09VwttY99ZJHFMuu/c5xzOF6PeoxhDM/wDA1p6DE7PIwWo8aZzWzmRV7kS7502N+YxtzP/dzN3XSkoym25ZPPcpbzKZ+ykIUUUlh2rC51eYRHeIEXuIzLTLGvFmgxagxyyeVFXuR93qcEI4lqd7rzBE9wJ3cSju/MEz3JST7iI6YwheMcL9vfkIa8zduMZCQWLCZaWCO0GDWKpSzlAR7gJCfL9vWgBy/zMrdwi4mWVc1FLjKd6fwP/+MgyjTS+IRPuIqrTLSu2ujpcMFOEUW8wiv0o1+ZEJvQhNnMJpNMnxciQDTRPMET7GUvL/MykUQCsJrVtKMdGWSYbGH10DVjEHOa0wxgAGtYA6hxvcd5nNd53a8H23exi9GMZiMbAXVfk5jERCaabFml6JoxWDnMYVJJLRNiQxqyhCVMZrJfCxGgDW1Ywxqe4zlCCEEQXuRFHuVRh76wr6FrxiDkBCe4iZvYxz4AUkhhKUv9pW9VIxaykHu5l4tcBOBhHuYDPvBFx46uGYONM5yhD33KhNiTnmSRFZBCBLiTO1nBChqgJqLPYAYv8ZLJVrlGizHIGMUotrIVgK50ZQlLiCPOVJs8TXe6s5SlZc3v13mdz/jMZKuc0WIMIqYxjUUsAqA1rVnMYr/vH1aXLnRhPvPLpueNYQwHOGCuUeXQYgwSDnCAp3kagCiiSCedy7ncZKu8y23cVtZEPcMZHuRBky1yRIsxSJjIxDInxlu8ZeqKhxYtWvDpp5+acu2JTKQ7KorAKlaxmMWm2OEKLcYgYAtbmMtcANrRjvGMN9WeqKgoIiMjTbl2KKFMYQoh1kd/AhOcJsGbhRZjEDCNaWUP3Fu8RSjeDZP/2Wefceutt/Ljjz8CEBkZSWRkJIWFhfz973/n5ptvprCwsIpvcR8d6cgwhgGwgx1kkum1a1eGFmOAc5GLZdPBmtOc27nd6zakpaWRmppKv379eOihh8jPz2fFihWkpKSwZs0aXnjhBa8HKn6cx8u2P+Ijr167QryW8EpjCotkkWD997q8bqot+fn5MnLkSAHkiiuukKysLFPtSZEUQZBYiZViKTbVFhFJ1zVjgLOWtWXbZtSKoNKCv/nmmyQnJxMWFkabNm0YNmwYDzzwAP3792f58uWV5vXwFLbyyCOPH/nR69cvjxZjgGObLB1DjGke1G+++YZVq1bxxRdfMHPmTKKiovjd737Hjh076NmzJ2+++aZX+4w2utkF7tnABq9fvzxajAGOLX5MEkled9zYGDp0KCtWrKB9+/YAFBQUUFBQQEREBE8//TTffPONKd7VZJLLtiuKs+NNtBgDHFsAqSu50mRLDAoKCsjPzzfbDIdJDw6BtkxCBzEOcGwD/dFEm2yJwd69e802AcBhKuB5zptoiULXjAGOLTDTaU6bbInvYR902RemBmoxBji2h8w+to1G8Qu/lG3blliZiRZjgNMKleZ7D3s4wxmTrfEtvsNI2tOGNiZaotBiDHBsk6JLKS0b5tAo1rO+bPtGbjTREoUWY4DTgx5l2+mkm2iJb5FPftnazmY084mEOlqMAU5nOpNEEgCf8ZlTaPxgZSELySEHgBGMMNkahRZjgGPBwv3cD8A5zvEu75prkA9QSilv8zagymcUo0y2SKHFGASMYUzZEMfbvO3gRQxG5jCHLWwBYChDaU5zky1SaDEGAQ1owAu8AMBZzvIIj5hskXmc5CQTmABABBG8xmsmW2SgxRgkjGc8rWkNwAIWMIMZJlvkfUopZTjDy8Zc/8gfaUELk60y0GIMEmxBqKKIApQ4l7PcZKu8y9M8zUpWAtCJTrzKqyZb5IgWYxCRQkqZ46KQQoYwhM1sNtkq7/AqrzKZyYCaIjiPeUQQYa5R5dBiDDIe47GykI1nOUsaaXzFVyZb5TkE4RVe4WVeBtSE+UUs4hquMdkyZ7QYg5D/5X/L3PnnOMcABjCHOSZb5X4ucIHhDOcv/AVQDpt5zCOVVJMtc40WYxBiwcIsZpXVFgUUMIpRjGRkwEwK2MUuutK1LERlDDEsYhH96W+yZRWjxRikWLDwCq/wDu+Uhbz/hE+4jutYwQqTrbt0CijgDd6gE53YxjYAEklkDWu4jdtMtq5ytBiDnCd5kkwyuZqrAcgmm1u5lbu52+dyUVTFUpbSnvYO0dMHMICtbKUDHcw1rhpoMWroRje2sIWRjCzLW5hBBkkk8TAPs5/9JltYOV/yJTdyI33pyx72ABBHHNOYxhd84RNrFauDTpaqcWANaxjHuLImHkAIIdzCLfyBPzCQgaYFtrLnLGeZxzymMa0sxZ2NIQxhClNoRCNzjLs0MrQYNU4UU8ynfMprvMZeHOPVNKUpQxjCIAZxAzd4NQPwec6zjGV8zuf8h/84xK2xYOEO7uBlXqYTnbxmkxvRYtRUTDHFzGUuU5nKt3zrdDyBBG7hFnrQg1RSy6IKuIsCCviO78gkkzWsIYussr6gjUgiGcQgnuIpfxWhDS1GTfXYwhamM535zK8wrGE96pFMMimkkEQS8cSTSCKNaEQ96hFFFHWpSwQRnOMcRRRx1vrvMIc5yUkOcpCd7GQ728kmm2KKXV6rHe0YyUhGM5oruMKTt+4ttBg1NaOEEjLJZAEL+IqvnJqxniKccDrTmX70YyADyxZMBxBajJracYxjZJLJetazne1sY5tDCEQndgOLgOcqPiWEEJrSlBRS6EAHUkmlK10DPeW5FqPG/ZzgBD/zM8c5zlGOcpKT5JFHAQXsSt/FmqFreEAeIIwwYoihHvVIIIF44kkggZa0DHThuSJDRxTXuJ3G1n+uSCedNaxhJjO9bJXvowf9NRofQYtRo/ERtBg1Gh9Bi1Gj8RG0GDUaH0GLUaPxEbQYNRofQYtRo/ERtBg1Gh9Bi1Gj8RG0GDUaH0GLUaPxEbQYNRofQYtRo/ERtBg1Gh9Bi1Gj8RG0GDUaH0GLUaPxEbQYNRofQYtRo/ERtBg1Gh9Bi1Gj8RG0GDU+ydatW+nbty9xcXHExsbSu3dv1q1bZ7ZZHkWLUeNzbNy4kW7duhEbG8uuXbvYv38/zZs3Jy0tjeXLl5ttnsfQEcU1XiU9PZ2hQ4dS0WNXWlpK+/btycnJYd++fURHRwNQUlJC27ZtuXDhAtnZ2URGRnrTbG+QoWtGjU+RlZXFjh07GDx4cJkQAUJDQxk2bBiHDx9m8eLFJlroObQYNT7FqlWrALj++uudjtn2rVy50qs2eQstRo1PsXv3bgCuuuoqp2MJCQkA7Nmzx6s2eQstRo1PkZubC0Ddus5ZqGJiYgA4ffq0N03yGlqMGr/B5vSxWCwmW+IZtBg1PkVcXBwA58+fdzpm22c7J9DQYtT4FK1btwbgyJEjTseOHj0KQFJSwKUQB7QYNT7GzTffDMDmzZudjtn29erVy6s2eQstRo1P0bNnT5KTk5k/fz75+fll+0tKSpg3bx6JiYn07dvXRAs9hxajxqcICQlh5syZ5OTkMHr0aE6cOMGpU6cYN24c2dnZzJgxg6ioKLPN9AhajBqfo2vXrqxfv54zZ87QqlUrmjZtSnZ2NqtXr+b3v/+92eZ5jDCzDdBoXNGxY0eWLl1qthleRdeMGo2PoMWo0fgIWowajY+gxajR+AhajBqNj6DFqNH4CFqMGo2PoMWo0fgIWowajY+gxajR+AhajBqNj6DFqNH4CFqMGo2PoMWo0fgIegmVxmMcPXqUlJQUioqKHPbXqVOH2NjYss8Wi4Ubb7yRr776ytsm+hRajBqPkZCQwDXXXMOmTZsqzK0BSox9+vTxomW+iW6majzKyJEjCQmp+jEbMmSIF6zxbbQYNR5l6NChlR4PCQmhR48eZaH7gxktRo1HufLKK0lLSyM0NNTlcYvFwogRI7xslW+ixajxOCNGjKiwz2ixWLjrrru8bJFvosWo8Th33XUXYWHOvsKwsDD69OlDgwYNTLDK99Bi1HiGUuA4sA3qZdejb5e+hIU6CrKkpIT7ut4Hm4Fs4KwJdvoQOo245tIpAHYC24DtwD6UAA8BJ4Fi49TP+ZwhDEEwHrdoovmN36hDHePEukAi0Bi4CmgDtANSgKZAYCagAsjQ44ya6nMAWA18A3wL7MVBcJXRl77UoQ7nUZmkwglnEIMchQhwHthtfZUnFiXMm4CeQA/rvgBB14yairkIfAn8GyXAg1WcHwo0AhKAeFQNdyUQBdSB++fez9zv5lJYXAjA0seW0qdZHygEzgFHUDXrEeAYUFVO1FCgE3ALMAhwzjzuT2RoMWocuQAsAeYDS1EiccXVQAdUTdXe+p5EpXO6li9fXhaev379+vz666+Eh4dX/AdngR2oZrCtKfw9Ffctm6JEORjogr81abUYNVaygZnADCDHxfF4VPOwN/A7oFnNL1FcXEyjRo3IycnhkUce4b333qv5l5SgmrDrgK+tL1c1aBLwAPAw4B/OWi3GoEaARcBUYJX1sz3XomqZQShHSkWUAPtRNdcB4Chwwu69ADgDlMLjZx5nSukUsqKySI1OVU3YaKA+0ATluEmwbicBbYGGlVy72Gr758AXwK/ljscA9wFPAK0r+R7z0WIMWv4D/BnYWm5/IvAH4B7gGhd/JygP6hpU7bQd2IUSXDVYz3ru4R4OcICQ6o6sXY4S5XVAKtAd1TctTwmQCcwBPgPy7Y6FAvei7tnVfZmPFmPQsQJ4EeUNtWEBegGPAv1RD649J1FOnCXAWuBUNa9lq+3qAPXU90qsMP3QdMY0HqMEfBElmt+s18mv8NscaQWkAQNQDpzIcsd/Az4C3kfV2jbCgJHAK6gfHt9BizFoOA48CaTb7bOgmqAvoxww9hwDPgUWAhtQg/iuSMQYC2yDago2QfUxo13/iYhgsVTgXcmx2noY5bzZhVH7VuS4iQVus97LnTgKsxTljPoLqka3EWPd9zi+spBQizHgEeAT4I84OmZ6A/+DavrZKEF5UD+0vpcfQwxB9SNTUc6cm1Ci8walKHFmoZrHWag+aXkuB0agHDfJ5f7+c+Al4Ce7/e2BD4Cu7je5hmgxBjTHgeGoMUIbbVFNt5vs9l1ECfCvqNkz9kShmrADUE1YV301s9iCckAtwrnvC6r5+gLKfhvFwBRU39E2bBOKarq/hHMT3XtoMQYs36CEeNz6ORr4E/A8RjPuPPAe8DdUf82eG1G1yxBUk87X2QfMsr6OlTt2IzAR6Gu37xgwAdVqsJEG/Avv1faOaDEGHAK8an3Z+nmdgblAC7tzPgOeRc12sVEHGA2MxbkP6S8Uo5rYU1BjkPb0Av6BY/M1A/Wjc8b6uTGqX53qWTNdoMUYUJSgPKLT7fb9AfVgRlg/bwfGo4YAbMRa/+4pKh/T8zc2Aq8DizHGUMNR9/8KysMLaprfUOv5oFoO/0SNsXqPDL2EKlDIRz08NiHGAAtQzokI1MP4N9T8TZsQw1CD4QeAtwgsIYKaEvdvYBNGH7kIeAc1le+/1n1Xo8pkjPVzATAMNTTiRbQYA4EC4A7UMATAFcBKwLaA/ihwK/AMxuD8zSgHyGT8ZbrYpXMdyvv6T9SwC6ixxx6o4Y1iVG34PjDJerwYeAg1O8lL6GaqvyPAKAxHRBPUSosU6+dNQD/UtDRQ6wXfQfWTgpGzqGaqvePmNlQ/0bYc62NU+RSjqqvP8EaTVTdT/Z5nMB6sBGA9hhC/RLn3bUK8HrWqPliFCKqfOAfluLG1CL4EumEM69yPaqJaUE6wEajpfx5Gi9GfmQH83bpdH+VFvNr6eRaq6Zpn/TwKNVjeypsG+jCDUQ6bJOvn7ag5r/usn0cAb1q381Eze8qPwboZLUZ/ZS/K+wnKQ5iBmk0Cqu/4B5R31YKa7jYLw6OqUVyDcuL0tH4+gupL20T3HGq6HKjZS8NRZeohtBj9kWLUsiDbDJK3UWsMAb5CuemLUUJ8D+XG96+Ftt6jAarMbNkFDlu3bZPh/4aaNABqkvxfPWeKFqM/8ibGmFgvjF/vw6hlQoXWz2+hBvA1lROJmrfaw/p5J6qZKqjhn08wnDt/RjVpPYAWo79xElUTAlyG8vyFoMbPhmBMBn8WNf1NUz2iUWOStplHyzBqwRbAu9btQtSUQg+gxehvvIrRPP0zKpwhqH6hrbbsgeF80FSf+qi+t20u7kSU9xnUNMFu1u3FqCh5bkaPM/oTP6PWDBaigi/tRjWx9qFWYxSg+kBbgP9njokBwTzUDBxQ/cV1qD53JmoyOagZPe4d7tDjjH7FBxj9wb9grL54GmNmzd/QQqwt96AmAoDytmZYt3tiOHrWon703IgWo79QglreA2oOqe2XeyNqPR+oaV8jvWxXoPI3jAgAL2CsgHnS7pzZ7r2kFqO/8BXGcqcRqLFFUPMpbbyNR/5HO3TogMViqfbrtddeIyYmxmn/X//qPC5w5MgRl9+xcOFCh/NefPFFp3N273YVdtxNJKP6iaC6ASus270xWh7/pNqBuKqFaPyD+0UE6+tH675cEalr3ddKREo9c+lrr71WMjIyHPaNGTNGAFm2bJnD/qFDh8qkSZNERGTLli0CyIABA6q8xty5cwWQ5557rtLzevbsKTNmzKjZDVwqP4hR5oPs9k+027/UbVdL1zWjv7DO+t4MY+7pArCmrlDzTfXAvntpj1qYDWrYw7YAub/dOevddznfiIulqZzfUNPfwHCvg1oWZKPybN21YuvWrdU+d968eZ4zxAyGAt+hxnHXoxw4HVHjkhcx1kS6AV0z+gP/xVip3sVuv8213gxjvFHjXuzDb9haJ+EYUfW+xW3zVbUY/YGf7bY7WN9PYaww6O5Va4KLjqg1oGBMqgDj/yEP+MU9l9Ji9AfsI3jbQmOcsNvnm+HqA4NwjGVp9mV+pd22q0RBl4AWoz9gL0bbgtjf7PZd4UVbghFb+VZU5tVNd1AFWoz+wBm77Tjre67dvsu8ZonHCA1V0YNLSirvgJWUlJSd6zVsP4D2NaB9mVeV1LWaaDH6A/aZti9Y3+va7TuP3xMTo2Znnz1bUUINRW5uLvXq1av0HLdjm5gf42IfOP5f1AItRn/gcrvtUy722Tef/JSkJBX/YseOHRWeU1BQwN69e2nZsqW3zFLYcj5W1DS1/7+oBVqM/oB9KEXbQ2D/YBzHbwkLC2P37t20aNGC1q1bs2HDBrKzs12em56ezpVXXkm7dl4Od24rXy1GjUPuB9tzehWGSDd41xxP8c477xASEkKfPn1YsGABOTk5lJSUcOzYMd577z3Gjx/P3//+d0JCvPjY7sMYukix22/7fwjD0bNaG9w2s07jOX4SYy7keLv9d1j3hYnIWe+YMmvWLEFNQXB45eXlOZxXt25dl+e5eu3atavs7zZv3iz33XefNG3aVCIjIyUiIkKuuuoqGTJkiKxbt847N2nPLDHK/hO7/Y2s+zq67UrpenGxPyCo8cXfgE6owMSg8itOsG7PRyUL1biXuzHWM+5HLereC9i6rY8C/+eWK+nFxX6BBSOZ548YDpu7MCaHf+hto4KA31ATxEEliW1q3V5pd44bk6xqMfoLt1vfizAWGSdhxPxcjkpgo3EfszDWK9pH2bMtKg7DCJHpBrQY/YV7USsFwHGFuS1UfykqKJXGPZzFiA4Xgyp/gD0YDrO+qHyObkKL0V+oj7GO7nvUagFQfZq21u1PUMt9NLXndQwv6pMYuRzfx1hBc797L6kdOP7E1xjNot4YoSAWozJNgerDrEGvVK0NO1BJgvJRNd8eVBDjI6iuwUVUtq8DGOFPao924PgVvVFZpUAJ05Ym+w4MkW4AXvSyXYHEeVRrI9/6+XWMaOKvoIQIKmat+4QI6JrR/9iAWu0vqDV136IeisOotXenUJ2PxRhhBTXV536MPnl/VBIhC/ADqrYsRg1r7MDdYtQ1o9/RFZWeDGArKsI4QCIq76Atp+AQjJXpmurxZwwhJmLkaMxHJRoqth57DbfXiqAdOP7JFIz5kG+gIl2DGv6wTQI4j/pl91CSloDj7xgpxOugMhnbyngCRjn2Qf3QeQAtRn8kAZhm3S5Fxfe0ef5eR+WiB7X+rheO4SI0zryJygANRq5L22D+QuAf1u1GqJrTQ1H4tBj9lSEY0cP3o8a8zqEelA9QIepBibQnKn+ExpFi1HS2F1B98BBUU982weI7VPNUUOX6Ie6bFO4CLUZ/ZhpGIs9NGElSbQ/VA9ZjBaisu69g9HuCnWPArRgtjGhU09T2I7YP5aW2Ldz+i/WzB9Fi9GfqoJpRLayfl6JmihSgmlszgcmo/+VS1APVHSMGa7CyEBWg+Bvr58tR0wltE+13o4aQbE3/h4GXPG+WFqO/0xCVh6OR9XMGyslgi17xBKqJWt/6+VvUyo//I/hqyZOooYu7MBYHX49qjt5k/bwJld/ykPXz7ahU7F5AizEQaIGaANDE+vkblOPGFlpwCGq1hy1N9llgPGqx7FfeM9M0ilCZh1thDF1YUOnX16GCQIPKVnwzRpiNAailaV6azaTFGCi0Q4Wfb2X9vAnVFFtu/fz/gFUob6stwNVuVB7CfgSmx7UItfKiLWp+qS3K3jWoZVDvAhEoB83/oMrBFmhqJEqI0XgNLcZA4mpU/g1bspZfUc2sV1F9xlCU53AXjmNli1Gu/N+hBOvvc7LOo5qWLVFOLFuIjBjUMMZ2VA0IqvXQGzWWWIKqMZ8HPsbr83v1dLhApAB4Cse+TnfUigP7WE7foPLWl0/ekoQaqxyFEcHcH9iEGn6Yi9FnBlX7jUQ5sGxNeUHlV3wGw1FTHzXrZqA3jHUiQ4sxkPkCVTPkWj+HocbVXscxBuhK1BSv1eX+PgLl/r8TNZvHg2Nsl8wPqMzNn6P6xfZEAQ8Cf8IxtfpeVDmssNvXETW0YV6qBC3GgGcf8AdU89NGU1QtMRzVdLWxETVhIB3nwMihqAnqt6IyM92AV/tTZZwA1qKa44tREx7K0wz1I/QQjot/f0X1DadirOAPB55GjcFGesTi6qLFGDRkAI+h3Ps2mqH6Sg/iKMqzqNAes1FDIaU4E4EaFrge5SBpi0q97a5UAwIcBHaiVkhsRzmoKhojjUENyj+A8iTbe0NyUFPa3sGx+XoTatDfy2FYK0CLMag4jVrrOAPlabSRjBrquA9j7Z6N46igTAtRfcyqctg3RMV5TUCNfV6FCn9fFyVg2/t5oBDlvSxC/Uj8Ahy1bh+i6rQFDVHN5wEoJ0xUuePZKLF9hGO+kiaoSeGj8aVsz1qMQcl+lIf1nzgO/NcDRqCCL7mqLS6gQn6sQzUV1+G2pC/VIh5Vm3W3vnfEeTygGNV8nYbqE9o/3Q1RLYGxmNPErhwtxqDmJ5QzJx3nGi8FGGx9JVfw96UoYe/EaE7+jKpNj2Gslq8Jl6FqriaoMdO2QBvUj0NFYfSLUH3i+SinVfkUbU1QA/zjcVuSGg+gxahBNQ9nopw3B10cT0aNV/ZEOW/quzjHFadRwryI0Sy1vcegnCe29waomq98U7Mifkat4/wGWIJzwlILkIbymt6JP8QE0mLU2FGCmmw+F9XUy3NxTiiqeZiKCvvRDlV7edITeQo1hLEdNZa4GhVmxBXNUbX5/aga1X/QYtRUQD7wJarptwTH5KzlCUPNdmmNCldha2ZehRqbrIPqo0VZtyMxHDdnUT8Ctlr0mPV1BCW4bVSdZaslasXFYNQkeP9Ei1FTDUpQ81jXoSakr8RteewvCZsjpzdqCl+zyk/3E7QYNZdACSqW6LZyr4PWY+6iDqq2TUE1h9tbt+Mr+yO/RYtR40ZKUGOER1BNy8Oo5u05lLf2AsqZk48aRgkF4lDDE3GoccnGqOZtExyTxAY+Gb7vY9L4D6EY/UVNjdFLqDQaH0GLUaPxEcIw8rJqNBrz2PD/AbetQncRG8WFAAAAAElFTkSuQmCC", + "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", "text/plain": [ - "" + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], - "metadata": {} + "source": [ + "from PIL import Image\n", + "file = Image.open(hnp.draw_graph(homomorphic_model))\n", + "file.show()\n", + "file.close()" + ] }, { "cell_type": "markdown", + "id": "de19a433", + "metadata": {}, "source": [ "### It's time to compile the function to its homomorphic equivalent" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 23, + "id": "cf89c63d", + "metadata": {}, + "outputs": [], "source": [ "engine = hnp.compile_numpy_function(\n", " infer,\n", " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", " inputset,\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "46753da7", + "metadata": {}, "source": [ "### Finally, let's make homomorphic inference" - ], - "metadata": {} + ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, + "id": "c0b246f7", + "metadata": {}, + "outputs": [], "source": [ "homomorphic_predictions = []\n", "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", " inference = QuantizedArray(engine.run(x_i), y_q.parameters)\n", " homomorphic_predictions.append(inference.dequantize())\n", "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "68f67b3f", + "metadata": {}, "source": [ "### And visualize it" - ], - "metadata": {} + ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, + "id": "92c7f2f5", + "metadata": {}, + "outputs": [], "source": [ "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "c18dbdd1", + "metadata": {}, "source": [ "### Enjoy!" - ], - "metadata": {} + ] } ], "metadata": { diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index b76c5e447..0235d0266 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -2,86 +2,109 @@ "cells": [ { "cell_type": "markdown", + "id": "9b835b74", + "metadata": {}, "source": [ "# Quantized Logistic Regression\n", "\n", "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "7d46edc9", + "metadata": {}, "source": [ "### Let's start by importing some libraries to develop our logistic regression model" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 1, + "id": "858205d9", + "metadata": {}, + "outputs": [], "source": [ "import numpy as np\n", "import torch" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "ff9c1757", + "metadata": {}, "source": [ "### And some helpers for visualization" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 2, + "id": "67330862", + "metadata": {}, + "outputs": [], "source": [ "%matplotlib inline\n", "\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "0df30d0e", + "metadata": {}, "source": [ "### We need an inputset, a handcrafted one for simplicity" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 3, + "id": "caef5aed", + "metadata": {}, + "outputs": [], "source": [ "x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n", "y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b16cd2e1", + "metadata": {}, "source": [ "### Let's visualize our inputset to get a grasp of it" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 4, + "id": "ad72aad0", + "metadata": {}, + "outputs": [], "source": [ "plt.ioff()\n", "fig, ax = plt.subplots(1)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 5, + "id": "ec57fede", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAPTklEQVR4nO3df4zkd13H8efruPPHpcgZbqO117v1D1ABKbQr1ED0lCgHmBJjTagVbCO5RKsup4mNEOkpaaIhchQbOC6lOdT1wNAGSgNGImAlhJo9LO2VCmmEKweNt7S5omBMznv7x3eW7q27O7N3szuzn30+ksnO9/v93Hxf/XTvtd/5zMxtqgpJ0sa3ZdQBJEnDYaFLUiMsdElqhIUuSY2w0CWpEVtHdeKdO3fW5OTkqE4vSRvS8ePHv1lVE0sdG1mhT05OMjs7O6rTS9KGlOTkcsdccpGkRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSI9ov9MW/M9XfoSqpUX0LPcnlST6V5ItJHk4yvcSYJHlXkkeTPJjkyrWJu0oHD8KBA0+XeFW3ffDgKFNJQzczA5OTsGVL93VmZtSJ2jeOcz7IFfpZ4A+q6nnA1cBNSZ63aMyrgOf0bvuB9ww15YWogjNn4Lbbni71Awe67TNnvFJXM2ZmYP9+OHmy+7Y+ebLbHoeCadW4znlqlcWW5CPA7VX1iQX73gt8uqqO9ba/BOytqseXe5ypqala838PfWGJz5uehkOHIFnbc0vrZHKyK5TF9uyBr351vdNsDqOc8yTHq2pqqWOrWkNPMgm8GLh/0aHLgK8t2D7V27f4z+9PMptkdm5ubjWnvjBJV94LWeZqzGOPrW6/Lt64zvnAhZ7kEuAu4E1V9a0LOVlVHamqqaqamphY8jcoDdf8FfpCC9fUpQbs3r26/bp44zrnAxV6km10ZT5TVXcvMeTrwOULtnf19o3OwuWW6Wk4d677unBNXWrArbfC9u3n79u+vduvtTGucz7Iu1wCvA94pKrescywe4A39N7tcjXw1Err5+sigR07zl8zP3So296xw2UXNeP66+HIkW79Num+HjnS7dfaGNc57/uiaJKXA/8MPASc6+1+M7AboKoO90r/dmAf8B3gxqpa8RXPdXlRtAt4fnkv3pakDWSlF0W39vvDVfUZYMUGrO6nwk0XFm+NLS5vy1xSo9r/pKgkbRIWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUiL6FnuTOJKeTnFjm+LOSfDTJF5I8nOTG4ceUJPUzyBX6UWDfCsdvAr5YVVcAe4G/SPI9Fx9NkrQafQu9qu4DnlxpCPDMJAEu6Y09O5x4kqRBDWMN/XbgJ4BvAA8B01V1bqmBSfYnmU0yOzc3N4RTS5LmDaPQXwk8APwI8CLg9iQ/sNTAqjpSVVNVNTUxMTGEU0uS5g2j0G8E7q7Oo8BXgB8fwuNKklZhGIX+GPAKgCQ/BPwY8O9DeFxJ0ips7TcgyTG6d6/sTHIKuAXYBlBVh4G3AUeTPAQEuLmqvrlmiSVJS+pb6FV1XZ/j3wB+cWiJJEkXxE+KSlIjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmN6FvoSe5McjrJiRXG7E3yQJKHk/zTcCNKkgYxyBX6UWDfcgeT7ADeDVxTVc8HfnUoySRJq9K30KvqPuDJFYb8GnB3VT3WG396SNkkSaswjDX05wI/mOTTSY4necNyA5PsTzKbZHZubm4Ip5YkzRtGoW8FrgJeA7wS+OMkz11qYFUdqaqpqpqamJgYwqklSfO2DuExTgFPVNW3gW8nuQ+4AvjyEB5bkjSgYVyhfwR4eZKtSbYDLwUeGcLjSpJWoe8VepJjwF5gZ5JTwC3ANoCqOlxVjyT5e+BB4BxwR1Ut+xZHSdLa6FvoVXXdAGPeDrx9KIkkSRfET4pKUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY3oW+hJ7kxyOsmJPuN+KsnZJNcOL54kaVCDXKEfBfatNCDJM4A/B/5hCJkkSRegb6FX1X3Ak32G/S5wF3B6GKEkSat30WvoSS4Dfhl4zwBj9yeZTTI7Nzd3saeWJC0wjBdF3wncXFXn+g2sqiNVNVVVUxMTE0M4tSRp3tYhPMYU8IEkADuBVyc5W1UfHsJjS5IGdNGFXlU/On8/yVHgXstcktZf30JPcgzYC+xMcgq4BdgGUFWH1zSdJGlgfQu9qq4b9MGq6oaLSiNJumB+UlSSGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJakTfQk9yZ5LTSU4sc/z6JA8meSjJZ5NcMfyYkqR+BrlCPwrsW+H4V4CfraqfBN4GHBlCLknSKm3tN6Cq7ksyucLxzy7Y/Bywawi5JEmrNOw19N8EPr7cwST7k8wmmZ2bmxvyqSVpcxtaoSf5ObpCv3m5MVV1pKqmqmpqYmJiWKeWJDHAkssgkrwQuAN4VVU9MYzHlCStzkVfoSfZDdwNvL6qvnzxkSRJF6LvFXqSY8BeYGeSU8AtwDaAqjoMvBV4NvDuJABnq2pqrQJLkpY2yLtcrutz/I3AG4eWSJJ0QfykqCQ1wkKXpEZY6JLUCAtdkhphoUtSIyx0SWqEhS5JjbDQJakRFrokNcJCl6RGWOiS1AgLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJaoSFLkmNsNAlqREWuiQ1wkKXpEZY6JLUiPYLvWrlbQ2fcy6NRN9CT3JnktNJTixzPEneleTRJA8muXL4MS/QwYNw4MDThVLVbR88OMpUbXPOtUnMzMDkJGzZ0n2dmRl1osGu0I8C+1Y4/irgOb3bfuA9Fx9rCKrgzBm47banC+bAgW77zBmvGteCc65NYmYG9u+Hkye7b+uTJ7vtkZd6VfW9AZPAiWWOvRe4bsH2l4BL+z3mVVddVWvu3Lmq6emqbs672/R0t19rwznXJrBnz/nf4vO3PXvW/tzAbC3Tq6kBrpqSTAL3VtULljh2L/BnVfWZ3vY/AjdX1ewSY/fTXcWze/fuq06ePHkBP4JWqap7TjTv3DlI1v68m5lzrsZt2bL0E86k+3ZfS0mOV9XUkrnW9tTnq6ojVTVVVVMTExPrccLuKf9CC9d3NXzOuTaB3btXt3+9DKPQvw5cvmB7V2/faC1cv52e7n5sTk+fv76r4XLOtUnceits337+vu3bu/2jtHUIj3EP8DtJPgC8FHiqqh4fwuNenAR27OgK5dChbvvQoe7Yjh0uAawF51ybxPXXd1/f8hZ47LHuyvzWW5/ePyp919CTHAP2AjuB/wBuAbYBVNXhJAFup3snzHeAG5daP19samqqZmf7Drt4VecXyeJtDZ9zLq2ZldbQ+16hV9V1fY4XcNMFZlt7i4vEYll7zrk0Eu1/UlSSNgkLXZIaYaFLUiMsdElqhIUuSY2w0CWpERa6JDXCQpekRljoktQIC12SGmGhS1IjLHRJasRAv7FoTU6czAHr8CuLvmsn8M11PN8wbdTsGzU3bNzsGzU3bNzs6517T1Ut+RuCRlbo6y3J7HL/5OS426jZN2pu2LjZN2pu2LjZxym3Sy6S1AgLXZIasZkK/cioA1yEjZp9o+aGjZt9o+aGjZt9bHJvmjV0SWrdZrpCl6SmWeiS1IimCj3JnUlOJzmxzPEkeVeSR5M8mOTK9c64nAGy703yVJIHere3rnfGpSS5PMmnknwxycNJppcYM3bzPmDucZ3z70vyL0m+0Mv+J0uM+d4kH+zN+f1JJkcQdXGmQXLfkGRuwZy/cRRZl5PkGUn+Ncm9Sxwb/ZxXVTM34GeAK4ETyxx/NfBxIMDVwP2jzryK7HuBe0edc4lclwJX9u4/E/gy8Lxxn/cBc4/rnAe4pHd/G3A/cPWiMb8NHO7dfx3wwQ2S+wbg9lFnXeG/4feBv13q+2Ic5rypK/Squg94coUhrwX+qjqfA3YkuXR90q1sgOxjqaoer6rP9+7/J/AIcNmiYWM37wPmHku9efyv3ua23m3xuxteC7y/d/9DwCuSZJ0iLmnA3GMryS7gNcAdywwZ+Zw3VegDuAz42oLtU2yQv8Q9P917uvrxJM8fdZjFek8xX0x35bXQWM/7CrlhTOe899T/AeA08ImqWnbOq+os8BTw7HUNuYQBcgP8Sm9p7kNJLl/fhCt6J/CHwLlljo98zjdboW9kn6f7NxyuAP4S+PBo45wvySXAXcCbqupbo84zqD65x3bOq+p/q+pFwC7gJUleMOJIAxkg90eByap6IfAJnr7iHakkvwScrqrjo86yks1W6F8HFv7E39XbN/aq6lvzT1er6mPAtiQ7RxwLgCTb6EpxpqruXmLIWM57v9zjPOfzquoM8Clg36JD353zJFuBZwFPrGu4FSyXu6qeqKr/6W3eAVy1ztGW8zLgmiRfBT4A/HySv1k0ZuRzvtkK/R7gDb13XVwNPFVVj4861CCS/PD8elySl9D9vxv5X9BepvcBj1TVO5YZNnbzPkjuMZ7ziSQ7eve/H/gF4N8WDbsH+I3e/WuBT1bv1bpRGST3otdWrqF7bWPkquqPqmpXVU3SveD5yar69UXDRj7nW9fzZGstyTG6dybsTHIKuIXuhReq6jDwMbp3XDwKfAe4cTRJ/78Bsl8L/FaSs8B/A68b9V/QnpcBrwce6q2NArwZ2A1jPe+D5B7XOb8UeH+SZ9D9kPm7qro3yZ8Cs1V1D90Pq79O8ijdi+2vG13c7xok9+8luQY4S5f7hpGlHcC4zbkf/ZekRmy2JRdJapaFLkmNsNAlqREWuiQ1wkKXpEZY6JLUCAtdkhrxf09l6LOTuZAtAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "x_min, x_max = x[:, 0].min(), x[:, 0].max()\n", "x_deviation = x_max - x_min\n", @@ -105,31 +128,22 @@ " color=\"blue\",\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "996fbe05", + "metadata": {}, "source": [ "### Now, we need a model so let's define it" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 6, + "id": "06ed91dd", + "metadata": {}, + "outputs": [], "source": [ "class Model(torch.nn.Module):\n", " def __init__(self, n):\n", @@ -139,22 +153,47 @@ " def forward(self, x):\n", " output = torch.sigmoid(self.fc(x))\n", " return output" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "cd74c5e7", + "metadata": {}, "source": [ "### And create one\n", "\n", "The main purpose of this tutorial is not to train a logistic regression model but to use it homomorphically. So we will not discuss about how the model is trained." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 7, + "id": "b8f8f95b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch: 1 | Loss: 1.0548723936080933\n", + "Epoch: 101 | Loss: 0.13212263584136963\n", + "Epoch: 201 | Loss: 0.07870622724294662\n", + "Epoch: 301 | Loss: 0.055601585656404495\n", + "Epoch: 401 | Loss: 0.04285423085093498\n", + "Epoch: 501 | Loss: 0.03481270745396614\n", + "Epoch: 601 | Loss: 0.029289430007338524\n", + "Epoch: 701 | Loss: 0.025266634300351143\n", + "Epoch: 801 | Loss: 0.02220827341079712\n", + "Epoch: 901 | Loss: 0.019805917516350746\n", + "Epoch: 1001 | Loss: 0.017869682982563972\n", + "Epoch: 1101 | Loss: 0.016276303678750992\n", + "Epoch: 1201 | Loss: 0.014942426234483719\n", + "Epoch: 1301 | Loss: 0.01380960550159216\n", + "Epoch: 1401 | Loss: 0.012835677713155746\n", + "Epoch: 1501 | Loss: 0.011989480815827847\n" + ] + } + ], "source": [ "model = Model(x.shape[1])\n", "\n", @@ -173,43 +212,22 @@ "\n", " if e % 100 == 1 or e == epochs:\n", " print(\"Epoch:\", e, \"|\", \"Loss:\", loss.item())" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Epoch: 1 | Loss: 0.568401038646698\n", - "Epoch: 101 | Loss: 0.13618899881839752\n", - "Epoch: 201 | Loss: 0.08024412393569946\n", - "Epoch: 301 | Loss: 0.05637403950095177\n", - "Epoch: 401 | Loss: 0.043313879519701004\n", - "Epoch: 501 | Loss: 0.035116538405418396\n", - "Epoch: 601 | Loss: 0.02950483374297619\n", - "Epoch: 701 | Loss: 0.025427138432860374\n", - "Epoch: 801 | Loss: 0.022332407534122467\n", - "Epoch: 901 | Loss: 0.01990474946796894\n", - "Epoch: 1001 | Loss: 0.01795022375881672\n", - "Epoch: 1101 | Loss: 0.016343189403414726\n", - "Epoch: 1201 | Loss: 0.014998838305473328\n", - "Epoch: 1301 | Loss: 0.013857820071280003\n", - "Epoch: 1401 | Loss: 0.012877327390015125\n", - "Epoch: 1501 | Loss: 0.012025881558656693\n" - ] - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "b608faef", + "metadata": {}, "source": [ "### Time to make some predictions" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 8, + "id": "97eaf932", + "metadata": {}, + "outputs": [], "source": [ "contour_plot_x_data = np.linspace(x_min - (x_deviation / 10), x_max + 2 * (x_deviation / 10), 100)\n", "contour_plot_y_data = np.linspace(y_min - (y_deviation / 10), y_max + 2 * (y_deviation / 10), 100)\n", @@ -217,20 +235,33 @@ "\n", "inputs = np.stack((contour_plot_x_data.flatten(), contour_plot_y_data.flatten()), axis=1)\n", "predictions = model(torch.tensor(inputs).float()).detach().numpy()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "8fb62d52", + "metadata": {}, "source": [ "### Let's visualize our predictions to see how our model performs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 9, + "id": "bc999411", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "contour = ax.contourf(\n", " contour_plot_x_data,\n", @@ -240,99 +271,97 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "cf05e044", + "metadata": {}, "source": [ "### As a bonus let's inspect the model parameters" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 10, + "id": "8f3236fb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[4.53867245]\n", + " [2.37340546]]\n", + "-14.672252655029297\n" + ] + } + ], "source": [ "w = np.array(model.fc.weight.flatten().tolist()).reshape((-1, 1))\n", "b = model.fc.bias.flatten().tolist()[0]\n", "\n", "print(w)\n", "print(b)" - ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "[[4.53581047]\n", - " [2.3700912 ]]\n", - "-14.660101890563965\n" - ] - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "44f1af11", + "metadata": {}, "source": [ "They are floating point numbers and we can't directly work with them!" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "dd440b8d", + "metadata": {}, "source": [ "### So, let's abstract quantization\n", "\n", "Here is a quick summary of quantization. We have a range of values and we want to represent them using small number of bits (n). To do this, we split the range into 2^n sections and map each section to a value. Here is a visualization of the process!" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 11, - "source": [ - "from IPython.display import SVG\n", - "SVG(filename=\"figures/QuantizationVisualized.svg\")" - ], + "id": "6314bb91", + "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { - "image/svg+xml": "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
", + "image/svg+xml": [ + "
min(x)
min(x)
max(x)
max(x)
Map
to 0
Map...
Map
to 1
Map...
Distance
Between
Consecutive
Values
Distan...
Map
to 2
Map...
Map
to 3
Map...
(when n = 2)
(when n = 2)
0
0
= 1 / scale
= 1 / q
= 1 / scale...
x = (x   + zp  ) / q
x = (x   + zp  ) / q
q
q
x
x
x
x
zero point
zp = 2
zero point...
Viewer does not support full SVG 1.1
" + ], "text/plain": [ "" ] }, + "execution_count": 11, "metadata": {}, - "execution_count": 11 + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "from IPython.display import SVG\n", + "SVG(filename=\"figures/QuantizationVisualized.svg\")" + ] }, { "cell_type": "markdown", + "id": "2c33faf9", + "metadata": {}, "source": [ "If you want to learn more, head to https://intellabs.github.io/distiller/algo_quantization.html" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 12, + "id": "9013f7e0", + "metadata": {}, + "outputs": [], "source": [ "class QuantizationParameters:\n", " def __init__(self, q, zp, n):\n", @@ -486,60 +515,66 @@ " def apply(self, x):\n", " assert x.parameters == self.input_parameters\n", " return QuantizedArray(self.table[x.values], self.output_parameters)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "477c431f", + "metadata": {}, "source": [ "### Let's quantize our model parameters\n", "\n", "Since the parameters only consist of scalars, we can use a single bit quantization." - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 13, + "id": "9a4dc030", + "metadata": {}, + "outputs": [], "source": [ "parameter_bits = 1\n", "\n", "w_q = QuantizedArray.of(w, parameter_bits)\n", "b_q = QuantizedArray.of(b, parameter_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "4592996c", + "metadata": {}, "source": [ "### And quantize our inputs" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 14, + "id": "5ba001fe", + "metadata": {}, + "outputs": [], "source": [ "input_bits = 5\n", "\n", "x = inputs\n", "x_q = QuantizedArray.of(inputs, input_bits)" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "5da2a8b9", + "metadata": {}, "source": [ "### Time to make quantized inference" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 15, + "id": "3f899676", + "metadata": {}, + "outputs": [], "source": [ "output_bits = 7\n", "\n", @@ -550,20 +585,33 @@ "y_q = sigmoid.apply(intermediate_q)\n", "\n", "quantized_predictions = y_q.dequantize()" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "4ea9b4fa", + "metadata": {}, "source": [ "### And visualize the results" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 16, + "id": "5d46b7d8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", @@ -576,31 +624,22 @@ " alpha=0.50,\n", ")\n", "display(fig)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "483c5c17", + "metadata": {}, "source": [ "### Now it's time to make the inference homomorphic" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 17, + "id": "f15f9e12", + "metadata": {}, + "outputs": [], "source": [ "q_y = (2**output_bits - 1) / (intermediate.max() - intermediate.min())\n", "zp_y = int(round(intermediate.min() * q_y))\n", @@ -616,12 +655,12 @@ "x_q = x_q.values\n", "w_q = w_q.values\n", "b_q = b_q.values" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "be208937", + "metadata": {}, "source": [ "### Simplification to rescue!\n", "\n", @@ -638,28 +677,32 @@ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "cannot be done on the circuit because of floating point operation so will be a single table lookup\n", "```" - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "34c675ed", + "metadata": {}, "source": [ "### Let's import the concrete numpy package now!" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 18, + "id": "72a84cac", + "metadata": {}, + "outputs": [], "source": [ "import concrete.numpy as hnp" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 19, + "id": "f8f197a2", + "metadata": {}, + "outputs": [], "source": [ "c1 = q_y / (q_x * q_w)\n", "c2 = w_q + zp_w\n", @@ -684,20 +727,22 @@ "\n", "def infer(x_0, x_1):\n", " return table[((x_0 + zp_x) * w_0) + ((x_1 + zp_x) * w_1)]" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "babb1a98", + "metadata": {}, "source": [ "### Let's compile our quantized inference function to it's operation graph for visualization" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 20, + "id": "b3a1d948", + "metadata": {}, + "outputs": [], "source": [ "inputset = []\n", "for x_i in x_q:\n", @@ -711,27 +756,25 @@ " },\n", " inputset,\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "ab5ba39e", + "metadata": {}, "source": [ "### Here are some representations of the operation graph" - ], - "metadata": {} + ] }, { "cell_type": "code", "execution_count": 21, - "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" - ], + "id": "13ac665b", + "metadata": {}, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": [ "%0 = Constant(2) # ClearScalar>\n", "%1 = Constant(1) # ClearScalar>\n", @@ -750,38 +793,48 @@ ] } ], - "metadata": {} + "source": [ + "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + ] }, { "cell_type": "code", "execution_count": 22, - "source": [ - "hnp.draw_graph(homomorphic_model).show()" - ], + "id": "52101260", + "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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", + "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", "text/plain": [ - "" + "" ] }, - "metadata": {} + "metadata": {}, + "output_type": "display_data" } ], - "metadata": {} + "source": [ + "from PIL import Image\n", + "file = Image.open(hnp.draw_graph(homomorphic_model))\n", + "file.show()\n", + "file.close()" + ] }, { "cell_type": "markdown", + "id": "aac3f0d4", + "metadata": {}, "source": [ "### It's time to compile the function to its homomorphic equivalent" - ], - "metadata": {} + ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, + "id": "ab75221c", + "metadata": {}, + "outputs": [], "source": [ "engine = hnp.compile_numpy_function(\n", " infer,\n", @@ -791,20 +844,22 @@ " },\n", " inputset,\n", ")" - ], - "outputs": [], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "972edbb0", + "metadata": {}, "source": [ "### Finally, let's make homomorphic inference" - ], - "metadata": {} + ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, + "id": "c83f68cd", + "metadata": {}, + "outputs": [], "source": [ "from tqdm.notebook import tqdm\n", "\n", @@ -815,35 +870,22 @@ " homomorphic_predictions.append(inference.dequantize())\n", " pbar.update(1)\n", "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" - ], - "outputs": [ - { - "output_type": "display_data", - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d0e8ac9eb4174f29978c3d4e4b6f42c9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/10000 [00:00" - ] - }, - "metadata": {} - } - ], - "metadata": {} + ] }, { "cell_type": "markdown", + "id": "52a83d37", + "metadata": {}, "source": [ "### Enjoy!" - ], - "metadata": {} + ] } ], "metadata": { From 6a83b01e924c685f6e22d80999a4ad1e91fd7bb6 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 16 Sep 2021 17:42:45 +0200 Subject: [PATCH 0269/1104] fix: check widths are supported by concrete-lib and if not, explain to the user refs #139 --- concrete/common/mlir/utils.py | 34 +++++++++++++++++++++++----- tests/numpy/test_compile.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 60779a3d7..9cfae6597 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -9,9 +9,13 @@ from ..data_types.dtypes_helpers import ( value_is_encrypted_tensor_integer, value_is_scalar_integer, ) +from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import ArbitraryFunction +# TODO: should come from compiler, through an API, #402 +ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB = 7 + def is_graph_values_compatible_with_mlir(op_graph: OPGraph) -> bool: """Make sure the graph outputs are unsigned integers, which is what the compiler supports. @@ -55,16 +59,36 @@ def update_bit_width_for_mlir(op_graph: OPGraph): op_graph: graph to update bit_width for """ max_bit_width = 0 + offending_list = [] for node in op_graph.graph.nodes: for value_out in node.outputs: if value_is_clear_scalar_integer(value_out) or value_is_clear_tensor_integer(value_out): - max_bit_width = max(max_bit_width, value_out.data_type.bit_width - 1) - elif value_is_encrypted_scalar_integer(value_out) or value_is_encrypted_tensor_integer( - value_out - ): - max_bit_width = max(max_bit_width, value_out.data_type.bit_width) + current_node_out_bit_width = value_out.data_type.bit_width - 1 + else: + + assert_true( + value_is_encrypted_scalar_integer(value_out) + or value_is_encrypted_tensor_integer(value_out) + ) + + current_node_out_bit_width = value_out.data_type.bit_width + + max_bit_width = max(max_bit_width, current_node_out_bit_width) + + # Check that current_node_out_bit_width is supported by the compiler + if current_node_out_bit_width > ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB: + offending_list.append((node, current_node_out_bit_width)) + _set_all_bit_width(op_graph, max_bit_width) + # Check that the max_bit_width is supported by the compiler + if len(offending_list) != 0: + raise RuntimeError( + f"max_bit_width of some nodes is too high for the current version of " + f"the compiler (maximum must be {ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB} " + f"which is not compatible with {offending_list})" + ) + def extend_direct_lookup_tables(op_graph: OPGraph): """Extend direct lookup tables to the maximum length the input bit width can support. diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index fe7da5ca7..14be89ef1 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -330,3 +330,45 @@ def test_compile_with_show_mlir(function, input_ranges, list_of_arg_names): data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), show_mlir=True, ) + + +def test_compile_too_high_bitwidth(): + """Check that the check of maximal bitwidth of intermediate data works fine.""" + + def function(x, y): + return x + y + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + "x": EncryptedScalar(Integer(64, False)), + "y": EncryptedScalar(Integer(64, False)), + } + + # A bit too much + input_ranges = [(0, 100), (0, 28)] + + with pytest.raises(RuntimeError) as excinfo: + compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + assert ( + "max_bit_width of some nodes is too high for the current version of the " + "compiler (maximum must be 7 which is not compatible with" in str(excinfo.value) + ) + + assert str(excinfo.value).endswith(", 8)])") + + # Just ok + input_ranges = [(0, 99), (0, 28)] + + compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) From 0e19e1aa44bf36a962a9029500e435440bccc092 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Thu, 16 Sep 2021 09:36:57 +0200 Subject: [PATCH 0270/1104] chore: upgrade sphinx-rtd-theme --- poetry.lock | 34 +++++++--------------------------- pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5d1987d45..fa591a9d8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1353,15 +1353,15 @@ test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-rtd-theme" -version = "0.5.2" +version = "1.0.0" description = "Read the Docs theme for Sphinx" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [package.dependencies] -docutils = "<0.17" -sphinx = "*" +docutils = "<0.18" +sphinx = ">=1.6" [package.extras] dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] @@ -1585,7 +1585,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "ada0e822f539bf819ccb21e005b48d868fad05746132a871a85549e2b7f8a0be" +content-hash = "945286da1d6371aafb20c064bfceaf978b9c40d8f19659c5be0ea0c599afd50a" [metadata.files] alabaster = [ @@ -1950,22 +1950,12 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1974,21 +1964,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1998,9 +1981,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2539,8 +2519,8 @@ sphinx = [ {file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"}, ] sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, - {file = "sphinx_rtd_theme-0.5.2.tar.gz", hash = "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a"}, + {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, + {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, diff --git a/pyproject.toml b/pyproject.toml index 36c5ad7f3..042650872 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ nbmake = "^0.5" flake8 = "^3.9.2" flake8-bugbear = "^21.4.3" Sphinx = "^4.1.1" -sphinx-rtd-theme = "^0.5.2" +sphinx-rtd-theme = "^1.0.0" myst-parser = "^0.15.1" nbsphinx = "^0.8.7" tqdm = "^4.62.2" From 14d7c505d804d2d1cb6a529dd4f6398a04b8517a Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Fri, 17 Sep 2021 13:54:24 +0200 Subject: [PATCH 0271/1104] docs: add admonitions, fixes, and nbsphinx syntax color # Conflicts: # docs/user/advanced_examples/QuantizedLinearRegression.ipynb # docs/user/advanced_examples/QuantizedLogisticRegression.ipynb --- docs/README.md | 26 +++++++++++------- docs/conf.py | 3 +++ docs/dev/explanation/COMPILATION.md | 8 +++--- docs/dev/explanation/FLOAT-FUSING.md | 2 +- docs/dev/explanation/MLIR.md | 2 +- .../explanation/TERMINOLOGY_AND_STRUCTURE.md | 2 +- docs/dev/howto/CONTRIBUTING.md | 8 ++++-- docs/dev/howto/DOCKER.md | 13 +++++---- docs/dev/howto/PROJECT_SETUP.md | 10 ++++--- docs/dev/howto/RELEASING.md | 4 +-- docs/index.rst | 3 +-- .../QuantizedLinearRegression.ipynb | 2 +- .../QuantizedLogisticRegression.ipynb | 2 +- .../explanation/FHE_AND_FRAMEWORK_LIMITS.md | 18 ++++++------- docs/user/explanation/QUANTIZATION.md | 6 +++-- docs/user/howto/COMPILING_AND_EXECUTING.md | 4 ++- .../user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md | 6 ++--- docs/user/howto/FAQ.md | 4 +-- docs/user/howto/INSTALLING.md | 27 ++++++------------- 19 files changed, 81 insertions(+), 69 deletions(-) diff --git a/docs/README.md b/docs/README.md index da76cc35f..3cdf1de02 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,31 +1,39 @@ -# What is ConcreteFHE +# What is **Concrete** ## Introduction -ConcreteFHE, or Concrete for short, is an open-source framework which aims to simplify the use of so-called fully homomorphic encryption (FHE) for data scientists. FHE is a new powerful cryptographic tool, which allows e.g. servers to perform computations directly on encrypted data, without needing to decrypt first. With FHE, privacy is at the center, and one can build services which ensure full privacy of the user and are the perfect equivalent of their unsecure counterpart. FHE is also a killer feature regarding data breaches: as anything done on the server is done over encrypted data, even if the server is compromised, there is in the end no leak of any kind of useful data. +**Concrete Framework**, or **Concrete** for short, is an open-source framework which aims to simplify the use of so-called fully homomorphic encryption (FHE) for data scientists. -The Concrete framework is made of several parts: +FHE is a new powerful cryptographic tool, which allows e.g. servers to perform computations directly on encrypted data, without needing to decrypt first. With FHE, privacy is at the center, and one can build services which ensure full privacy of the user and are the perfect equivalent of their unsecure counterpart. + +FHE is also a killer feature regarding data breaches: as anything done on the server is done over encrypted data, even if the server is compromised, there is in the end no leak of any kind of useful data. + +**Concrete** is made of several parts: - a library, called concrete-lib, which contains the core cryptographic API's for computing with FHE - a compiler, called concrete-compiler, which allows to turn an MLIR program into an FHE program, on the top of concrete-lib - some frontends, which convert different langages to MLIR, to finally be compiled. -In the first version of Concrete framework, there is a single frontend, called homomorphic numpy (or hnp), which is the equivalent of numpy. With our toolchain, a data scientist can convert a numpy program into an FHE program, without any a-priori knowledge on cryptography. +```{important} +In the first version of Concrete, there is a single frontend, called homomorphic numpy (or hnp), which is the equivalent of numpy. With our toolchain, a data scientist can convert a numpy program into an FHE program, without any a-priori knowledge on cryptography. +``` ## Organization of the documentation Basically, we have divided our documentation into several parts: - one about basic elements, notably description of the installation, that you are currently reading -- one dedicated to _users_ of Concrete, with tutorials, how-to's and deeper explanations -- and finally, one dedicated to _developpers_ of Concrete, who could be internal or external contributors to the framework +- one dedicated to _users_ of **Concrete**, with tutorials, how-to's and deeper explanations +- and finally, one dedicated to _developpers_ of **Concrete**, who could be internal or external contributors to the framework ## A work in progress +```{note} Concrete is a work in progress, and is currently limited to a certain number of operators and features. In the future, there will be improvements as described in this [section](user/explanation/FUTURE_FEATURES.md). +``` The main _current_ limits are: -- Concrete is only supporting unsigned integers -- Concrete needs the integer to be less than 7 bits (included) -- Concrete is mostly restricted to scalars (by opposition to tensors). The only exception is the `dot` operator, which can dot a tensor of encrypted values with a tensor of constant values +- **Concrete** is only supporting unsigned integers +- **Concrete** needs the integer to be less than 7 bits (included) +- **Concrete** is mostly restricted to scalars (by opposition to tensors). The only exception is the `dot` operator, which can dot a tensor of encrypted values with a tensor of constant values The first two limits can be taken care of with the use of quantization, as explained a bit further in [this](user/explanation/QUANTIZATION.md) and [this](user/howto/REDUCE_NEEDED_PRECISION.md) parts of the documentation. diff --git a/docs/conf.py b/docs/conf.py index 1dc663ff0..8f206847e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,6 +54,9 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # Group member variables and methods separately (not alphabetically) autodoc_member_order = "groupwise" +# -- Options for nbsphinx ---------------------------------------------------- + +nbsphinx_codecell_lexer = 'ipython3' # -- Options for HTML output ------------------------------------------------- diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index 664d3e57a..253edd7e8 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -1,8 +1,8 @@ # Compilation Pipeline In Depth -## What is concretefhe? +## What is **concretefhe**? -`concretefhe` is the python API of the `concrete` framework for developing homomorphic applications. +**concretefhe** is the python API of the **Concrete** framework for developing homomorphic applications. One of its essential functionalities is to transform Python functions to their `MLIR` equivalent. Unfortunately, not all python functions can be converted due to the limits of current product (we are in the alpha stage), or sometimes due to inherent restrictions of FHE itself. However, one can already build interesting and impressing use cases, and more will be available in further versions of the framework. @@ -104,7 +104,7 @@ Tracing is also responsible for indicating whether the values in the node would The goal of topological transforms is to make more functions compilable. -With the current version of `concrete` floating point inputs and floating point outputs are not supported. +With the current version of **Concrete** floating point inputs and floating point outputs are not supported. However, if the floating points operations are intermediate operations, they can sometimes be fused into a single table lookup from integer to integer thanks to some specific transforms. Let's take a closer look at the transforms we perform today. @@ -199,7 +199,7 @@ Assigned Data Types: ## MLIR conversion -The actual compilation will be done by the concrete compiler, which is expecting an MLIR input. The MLIR conversion goes from an operation graph to its MLIR equivalent. You can read more about it [here](./MLIR.md) +The actual compilation will be done by the **Concrete** compiler, which is expecting an MLIR input. The MLIR conversion goes from an operation graph to its MLIR equivalent. You can read more about it [here](./MLIR.md) ## Example walkthrough #1 diff --git a/docs/dev/explanation/FLOAT-FUSING.md b/docs/dev/explanation/FLOAT-FUSING.md index a283764e9..c283246a4 100644 --- a/docs/dev/explanation/FLOAT-FUSING.md +++ b/docs/dev/explanation/FLOAT-FUSING.md @@ -34,7 +34,7 @@ The simplified graph of operations with the float subgraph condensed in an `Arbi ![](../../_static/float_fusing_example/after.png) -## How is it done in concretefhe? +## How is it done in **Concrete**? The first step consists in detecting where we go from floating point computation back to integers. This allows to identify the potential terminal node of the float subgraph we are going to fuse. diff --git a/docs/dev/explanation/MLIR.md b/docs/dev/explanation/MLIR.md index 4c8f520ce..6cc33f6a7 100644 --- a/docs/dev/explanation/MLIR.md +++ b/docs/dev/explanation/MLIR.md @@ -1,6 +1,6 @@ # MLIR -MLIR is the intermediate representation used by the concrete compiler, so we need to convert the operation graph to MLIR, which will look something like the following, for a graph performing the dot between two input tensors. +MLIR is the intermediate representation used by the **Concrete** compiler, so we need to convert the operation graph to MLIR, which will look something like the following, for a graph performing the dot between two input tensors. ``` func @main(%arg0: tensor<4xi7>, %arg1: tensor<4x!HLFHE.eint<6>>) -> !HLFHE.eint<6> { diff --git a/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md index 1d487fb52..706986fef 100644 --- a/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md +++ b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md @@ -15,7 +15,7 @@ In this section we will go over some terms that we use throughout the project. ## Module structure -In this section, we will discuss the module structure of `concretefhe` briefly. You are encouraged to check individual `.py` files to learn more! +In this section, we will discuss the module structure of **concretefhe** briefly. You are encouraged to check individual `.py` files to learn more! - concrete - common: types and utilities that can be used by multiple frontends (e.g., numpy, torch) diff --git a/docs/dev/howto/CONTRIBUTING.md b/docs/dev/howto/CONTRIBUTING.md index d4c401488..128ce56b6 100644 --- a/docs/dev/howto/CONTRIBUTING.md +++ b/docs/dev/howto/CONTRIBUTING.md @@ -1,9 +1,11 @@ # Contributing -There are two ways to contribute to `concretefhe`: +```{important} +There are two ways to contribute to **concretefhe**: - you can open issues to report bugs, typos and suggest ideas - you can ask to become an official contributor by emailing hello@zama.ai. Only approved contributors can send pull requests, so please make sure to get in touch before you do! +``` Let's go over some other important things that you need to be careful about. @@ -26,7 +28,7 @@ git checkout -b fix/tracing_indexing_42 ### Conformance -Each commit to `concretefhe` should be comformant to the standards decided by the team. Conformance can be checked using the following commands. +Each commit to **concretefhe** should be conformant to the standards decided by the team. Conformance can be checked using the following commands. ```shell make pcc @@ -75,7 +77,9 @@ To learn more about conventional commits, check [this](https://www.conventionalc ## Before creating pull request +```{important} We remind that only official contributors can send pull requests. To become such an official contributor, please email hello@zama.ai. +``` You should rebase on top of `main` branch before you create your pull request. This is to avoid merge commits and have a clean git log. After you commit your changes to your new branch, you can use the following commands to rebase. diff --git a/docs/dev/howto/DOCKER.md b/docs/dev/howto/DOCKER.md index 5c64a8a1c..fbbe5ebcc 100644 --- a/docs/dev/howto/DOCKER.md +++ b/docs/dev/howto/DOCKER.md @@ -6,15 +6,18 @@ Before you start this section, go ahead and install docker. You can follow [this ### Linux -```console +```shell xhost +localhost ``` ### Mac OS -To be able to use X forwarding on Mac OS: first, you need to install xquartz. Secondly, open XQuartz.app application, and open a new terminal within XQuartz.app. Make sure in the application parameters to authorize network connections are set (currently in the Security settings); finally, in the XQuartz.app terminal, type +To be able to use X forwarding on Mac OS: +- Install XQuartz +- Open XQuartz.app application, make sure in the application parameters that `authorize network connections` are set (currently in the Security settings) +- Open a new terminal within XQuartz.app and type: -```console +```shell xhost +127.0.0.1 ``` @@ -24,13 +27,13 @@ and now, the X server should be all set in docker (in the regular terminal). Install Xming and use Xlaunch: - Multiple Windows, Display number: 0 -- Start no client +- `Start no client` - **IMPORTANT**: Check `No Access Control` - You can save this configuration to re-launch easily, then click finish. ## Logging in and building the image -Docker image of `concretefhe` is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. +Docker image of **Concrete** is based on another docker image provided by the compiler team. Once you have access to this repository you should be able to launch the commands to build the dev docker image with `make docker_build`. Upon joining to the team, you need to log in using the following command: diff --git a/docs/dev/howto/PROJECT_SETUP.md b/docs/dev/howto/PROJECT_SETUP.md index f411074aa..7bf215062 100644 --- a/docs/dev/howto/PROJECT_SETUP.md +++ b/docs/dev/howto/PROJECT_SETUP.md @@ -3,7 +3,7 @@ ## Installing Python v3.8 -`concretefhe` is a `Python` library. So `Python` should be installed to develop `concretefhe`. `v3.8` is the only supported version. +**concretefhe** is a `Python` library. So `Python` should be installed to develop **concretefhe**. `v3.8` is the only supported version. You can follow [this](https://realpython.com/installing-python/) guide to install it (alternatively you can google `how to install python 3.8`). @@ -21,7 +21,7 @@ On Linux you can install make from your distribution's preferred package manager On Mac OS you can install a more recent version of make via brew: -```bash +```shell # check for gmake which gmake # If you don't have it, it will error out, install gmake @@ -34,11 +34,13 @@ It is possible to install gmake as make, check this [StackOverflow post](https:/ On Windows check [this GitHub gist](https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make). -**/!\\ In the next sections, be sure to use the proper `make` tool for your system, `make`, `gmake` or other. /!\\** +```{hint} +In the next sections, be sure to use the proper `make` tool for your system, `make`, `gmake` or other. +``` ## Cloning repository -Now, it's time to get the source code of `concretefhe`. You can use the following command to do that. +Now, it's time to get the source code of **concretefhe**. You can use the following command to do that. ```shell git clone https://github.com/zama-ai/concretefhe-internal.git diff --git a/docs/dev/howto/RELEASING.md b/docs/dev/howto/RELEASING.md index 086c3733f..98955c483 100644 --- a/docs/dev/howto/RELEASING.md +++ b/docs/dev/howto/RELEASING.md @@ -2,8 +2,8 @@ ## Release Candidate cycle -Before settling for a final release, we go through a Release Candidate (RC) cycle. The idea is that once the code base and documentations look ready for a release you create an RC Release by opening an issue with the release template here: https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md starting with version `vX.Y.Zrc1` and then with versions `vX.Y.Zrc2`, `vX.Y.Zrc3`... +Before settling for a final release, we go through a Release Candidate (RC) cycle. The idea is that once the code base and documentations look ready for a release you create an RC Release by opening an issue with the release template [here](https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md), starting with version `vX.Y.Zrc1` and then with versions `vX.Y.Zrc2`, `vX.Y.Zrc3`... ## Proper release -Once the last RC is deemed ready, open an issue with the release template using the last RC version from which you remove the `rc?` part (i.e. `v12.67.19` if your last RC version was `v12.67.19-rc4`): https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md. +Once the last RC is deemed ready, open an issue with the release template using the last RC version from which you remove the `rc?` part (i.e. `v12.67.19` if your last RC version was `v12.67.19-rc4`) on [github](https://github.com/zama-ai/concretefhe-internal/issues/new?assignees=&labels=&template=release.md). diff --git a/docs/index.rst b/docs/index.rst index a46b4c6ba..7640582c7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ Concrete Framework's documentation :caption: Basics README.md - Installing + user/howto/INSTALLING.md .. toctree:: :maxdepth: 2 @@ -21,7 +21,6 @@ Concrete Framework's documentation :maxdepth: 2 :caption: How to - user/howto/INSTALLING.md user/howto/COMPILING_AND_EXECUTING.md user/howto/REDUCE_NEEDED_PRECISION.md user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index 56adac9ca..f92c68968 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -7,7 +7,7 @@ "source": [ "# Quantized Linear Regression\n", "\n", - "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" + "Currently, **Concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a linear regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" ] }, { diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index 0235d0266..03db2b1f9 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -7,7 +7,7 @@ "source": [ "# Quantized Logistic Regression\n", "\n", - "Currently, **concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" + "Currently, **Concrete** only supports unsigned integers up to 7-bits. Nevertheless, we want to evaluate a logistic regression model with it. Luckily, we can make use of **quantization** to overcome this limitation!" ] }, { diff --git a/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md b/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md index fa4c9cfc7..8a42d1a13 100644 --- a/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md +++ b/docs/user/explanation/FHE_AND_FRAMEWORK_LIMITS.md @@ -1,32 +1,32 @@ -# FHE and Concrete Framework Limits +# FHE and **Concrete Framework** Limits ## FHE limits -FHE used to be an impossible thing to imagine, twenty years ago. Then, with advances due to [Craig Gentry](https://crypto.stanford.edu/craig/), this became a dream come true. And, even more recently, with several generations of new scheme, FHE became practical. +FHE used to be an impossible thing to imagine, twenty years ago. Then, with advances due to [Craig Gentry](https://crypto.stanford.edu/craig/), this became a dream come true. And, even more recently, with several generations of new scheme, FHE became practical. ### Speed -However, one still has to consider that FHE is slow, as compared to the vanilla implementations. With the different HW pluggins that can be added to Concrete, an important speed factor can be achieved. +However, one still has to consider that FHE is slow, as compared to the vanilla implementations. With the different HW pluggins that can be added to **Concrete**, an important speed factor can be achieved. ### Multiplying by constants -In the scheme used in the Concrete Framework, namely [TFHE](https://tfhe.github.io/tfhe/), multiplications by constants is only defined for integer constants. Notably, one can't multiply by floats. As float multiplication is very usual in the data science (think of weights of dense layers, for example), this could be a problem, but quantization is at our rescue. See [this](QUANTIZATION.md) section for more details. +In the scheme used in the **Concrete Framework**, namely [TFHE](https://tfhe.github.io/tfhe/), multiplications by constants is only defined for integer constants. Notably, one can't multiply by floats. As float multiplication is very usual in the data science (think of weights of dense layers, for example), this could be a problem, but quantization is at our rescue. See [this](QUANTIZATION.md) section for more details. ### Achieving computations of not-linear functions -For most FHE scheme but TFHE, the application of a non-linear function is complicated and slow, if not impossible. Typically, this is a blocker, since activation functions _are_ non-linear. However, in the Concrete Framework, we use an operation called _programmable bootstrapping_ (described in this [white paper](https://whitepaper.zama.ai)), which allows to apply any table lookup: by quantizing the non-linear function, any function can thus be replaced. +For most FHE scheme but TFHE, the application of a non-linear function is complicated and slow, if not impossible. Typically, this is a blocker, since activation functions _are_ non-linear. However, in the **Concrete Framework**, we use an operation called _programmable bootstrapping_ (described in this [white paper](https://whitepaper.zama.ai)), which allows to apply any table lookup: by quantizing the non-linear function, any function can thus be replaced. -## Concrete Framework limits +## **Concrete Framework** limits -Since this is an early version of the product, not everything is done, to say the least. What we wanted to tackle first was the cryptographic complexities. This is why we concentrated on the cryptographic part, and let some engineering problems for later. +Since this is an early version of the product, not everything is done, to say the least. What we wanted to tackle first was the cryptographic complexities. This is why we concentrated on the cryptographic part, and let some engineering problems for later. ### Limited to scalars -Today, the Concrete Framework is mostly limited to scalars. Notably, in our numpy frontend, we can not use [tensors](https://numpy.org/doc/stable/user/theory.broadcasting.html?highlight=vector). As explained in [this section](FUTURE_FEATURES.md), this limit will be removed in the next version. +Today, the **Concrete Framework** is mostly limited to scalars. Notably, in our numpy frontend, we can not use [tensors](https://numpy.org/doc/stable/user/theory.broadcasting.html?highlight=vector). As explained in [this section](FUTURE_FEATURES.md), this limit will be removed in the next version. ### Currently executing locally -As of today, the execution of the FHE program is done locally. Notably, in the current version, there is no client (on which we encrypt the private data, or decrypt the returned result) or server (on which the computation is done completely over encrypted data), but a single host. As explained in [this section](FUTURE_FEATURES.md), this limit will be removed in the next version, such that the Concrete Framework can be used in production. +As of today, the execution of the FHE program is done locally. Notably, in the current version, there is no client (on which we encrypt the private data, or decrypt the returned result) or server (on which the computation is done completely over encrypted data), but a single host. As explained in [this section](FUTURE_FEATURES.md), this limit will be removed in the next version, such that the **Concrete Framework** can be used in production. ### Currently slow diff --git a/docs/user/explanation/QUANTIZATION.md b/docs/user/explanation/QUANTIZATION.md index 52b7df871..585867392 100644 --- a/docs/user/explanation/QUANTIZATION.md +++ b/docs/user/explanation/QUANTIZATION.md @@ -1,8 +1,10 @@ # Quantization -From Wikipedia https://en.wikipedia.org/wiki/Quantization : +```{note} +from [Wikipedia](https://en.wikipedia.org/wiki/Quantization): > Quantization is the process of constraining an input from a continuous or otherwise large set of values (such as the real numbers) to a discrete set (such as the integers). +``` ## Why is it needed? @@ -14,7 +16,7 @@ The basic idea of quantization is to take a range of values represented by a _la ## Quantization in practice -To quantize a range of values on a smaller range of values, we first need to choose the data type that is going to be used. ConcreteLib, the library used in the Concrete Framework, is currently limited to 7 bits unsigned integers, so we'll use that for the example. Knowing that, for a value in the range `[min_range, max_range]`, we can compute the step of the quantization, which is `(max_range - min_range) / (2**n - 1)` where n is the number of bits, here 7, so in practice the quantization step is `step = (max_range - min_range) / 127`. This means the gap between consecutive representible values cannot be smaller than that `step` value which means there can be a substantial loss of precision. Every interval of length `step = (max_range - min_range) / 127` will be represented by a value in `[0..127]`. +To quantize a range of values on a smaller range of values, we first need to choose the data type that is going to be used. **ConcreteLib**, the library used in the **Concrete Framework**, is currently limited to 7 bits unsigned integers, so we'll use that for the example. Knowing that, for a value in the range `[min_range, max_range]`, we can compute the step of the quantization, which is `(max_range - min_range) / (2**n - 1)` where n is the number of bits, here 7, so in practice the quantization step is `step = (max_range - min_range) / 127`. This means the gap between consecutive representible values cannot be smaller than that `step` value which means there can be a substantial loss of precision. Every interval of length `step = (max_range - min_range) / 127` will be represented by a value in `[0..127]`. The IntelLabs distiller quantization documentation goes into a detailed explanation about the math to quantize values and how to keep computations consistent: [quantization algorithm documentation](https://intellabs.github.io/distiller/algo_quantization.html). diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index e349bdba7..42abed017 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -10,7 +10,7 @@ import concrete.numpy as hnp ## Defining a function to compile -You need to have a python function that follows the [limits](../explanation/FHE_AND_FRAMEWORK_LIMITS.md) of the Concrete Framework. Here is a simple example: +You need to have a python function that follows the [limits](../explanation/FHE_AND_FRAMEWORK_LIMITS.md) of the **Concrete Framework**. Here is a simple example: ```python def f(x, y): @@ -58,8 +58,10 @@ You can use `.run(...)` method of `engine` returned by `hnp.compile_numpy_functi 0 ``` +```{caution} Be careful about the inputs, though. If you were to run with values outside the range of the inputset, the result might not be correct. +``` ## Further reading diff --git a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md index 3869b7368..b87f1c7cb 100644 --- a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md +++ b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -1,13 +1,13 @@ # Debugging / Support / Submitting Issues -First, let's not forget that this version of Concrete framework is a beta product, meaning that it is not completely polished, contains several bugs (would they be known or unknown at this time). Also, let's not forget that FHE is a highly hot topic, and notably, that it cannot be considered as a solved problem. +First, let's not forget that this version of **Concrete** is a beta product, meaning that it is not completely polished, contains several bugs (would they be known or unknown at this time). Also, let's not forget that FHE is a highly hot topic, and notably, that it cannot be considered as a solved problem. Anyway, let's list some ways to debug your problems here. If nothing seems conclusive, you can still report the issue, as explained in a later section of this page. ## Is it a bug by the framework or by the user? If ever your numpy program fails, it may be because: -- of bugs due to Concrete framework +- of bugs due to **Concrete** - of bugs due to the user, notably who would have a bug without even considering FHE (does the function you want to compile run well with numpy?), or who would not use the framework as expected or not consider the limits of the framework. For the latter kind of bugs, we encourage the user to have a look at: @@ -58,7 +58,7 @@ In order to simplify our work and let us reproduce your bug easily, any informat - the reproducibility rate you see on your side - any insight you might have on the bug - any workaround you have been able to find -may be useful to us. Don't remember, Concrete is a project where we are open to contribution, more information at Contributing (TODO: add a link). +may be useful to us. Don't remember, **Concrete** is a project where we are open to contribution, more information at Contributing (TODO: add a link). ## Submitting an issue diff --git a/docs/user/howto/FAQ.md b/docs/user/howto/FAQ.md index 7b976a960..eb5b82ffe 100644 --- a/docs/user/howto/FAQ.md +++ b/docs/user/howto/FAQ.md @@ -1,6 +1,6 @@ # FAQ -## What is Concrete FHE? +## What is **Concrete**? See [here](../../README.md) @@ -10,5 +10,5 @@ See [here](../../README.md) ## Can I contribute? -## What are the future features of Concrete? +## What are the future features of **Concrete**? diff --git a/docs/user/howto/INSTALLING.md b/docs/user/howto/INSTALLING.md index 126e2b5e1..af1e45adc 100644 --- a/docs/user/howto/INSTALLING.md +++ b/docs/user/howto/INSTALLING.md @@ -2,7 +2,9 @@ ## Docker image +```{note} Currently the project is only available as a docker image. To get the image you need to login to ghcr.io with docker. +``` ```shell docker login ghcr.io @@ -13,38 +15,25 @@ This command will ask for a username and a password. For username, just enter yo You can then either pull the latest docker image or a specific version: ```shell -docker pull ghcr.io/zama-ai/concretefhe-internal:latest +docker pull ghcr.io/zama-ai/concretefhe:latest # or -docker pull ghcr.io/zama-ai/concretefhe-internal:v0.1.0 +docker pull ghcr.io/zama-ai/concretefhe:v0.1.0 ``` You can then use this image with the following command: ```shell # Without local volume: -docker run --rm -it -p 8888:8888 ghcr.io/zama-ai/concretefhe-internal:v0.1.0 +docker run --rm -it -p 8888:8888 ghcr.io/zama-ai/concretefhe:v0.1.0 # With local volume to save notebooks on host: -docker run --rm -it -p 8888:8888 -v /host/path:/data ghcr.io/zama-ai/concretefhe-internal:v0.1.0 +docker run --rm -it -p 8888:8888 -v /host/path:/data ghcr.io/zama-ai/concretefhe:v0.1.0 ``` -This will launch a concretefhe enabled jupyter server in the docker, that you can access from your browser. +This will launch a **Concrete** enabled jupyter server in the docker, that you can access from your browser. Alternatively you can just open a shell in the docker: ```shell -docker run --rm -it ghcr.io/zama-ai/concretefhe-internal:v0.1.0 /bin/bash - -root@e2d6c00e2f3d:/data# python3 -Python 3.8.10 (default, Jun 2 2021, 10:49:15) -[GCC 9.4.0] on linux -Type "help", "copyright", "credits" or "license" for more information. ->>> import concrete.numpy as hnp ->>> dir(hnp) -['ClearScalar', 'ClearTensor', 'CompilationArtifacts', 'CompilationConfiguration', 'EncryptedScalar', 'EncryptedTensor', 'Float', 'Float32', 'Float64', 'Integer', 'LookupTable', 'ScalarValue', 'SignedInteger', 'TensorValue', 'UnsignedInteger', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'compile', 'compile_numpy_function', 'compile_numpy_function_into_op_graph', 'draw_graph', 'get_printable_graph', 'np_dtypes_helpers', 'trace_numpy_function', 'tracing'] ->>> +docker run --rm -it ghcr.io/zama-ai/concretefhe:v0.1.0 /bin/bash ``` - - - - From b950bb4459af621574a3c8caab7559d07072a550 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 17 Sep 2021 15:08:34 +0300 Subject: [PATCH 0272/1104] fix: assign proper permissions to temporary drawings --- concrete/common/debugging/drawing.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index 305b75ed7..cb088c24b 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -1,5 +1,6 @@ """functions to draw the different graphs we can generate in the package, eg to debug.""" +import os import tempfile from pathlib import Path from typing import Optional @@ -90,6 +91,23 @@ def draw_graph( if save_to is None: with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + # we need to change the permissions of the temporary file + # so that it can be read by all users + + # (https://stackoverflow.com/a/44130605) + + # get the old umask and replace it with 0o666 + old_umask = os.umask(0o666) + + # restore the old umask back + os.umask(old_umask) + + # combine the old umask with the wanted permissions + permissions = 0o666 & ~old_umask + + # set new permissions + os.chmod(tmp.name, permissions) + save_to_str = str(tmp.name) else: save_to_str = str(save_to) From 0a6ebf3b1946d78800b9d2a519883e16aaace8cb Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 17 Sep 2021 17:03:50 +0200 Subject: [PATCH 0273/1104] workaround: make compilation work if table output is small this workaround will be removed once it is managed by the compiler. closes #279 refs #412 --- concrete/common/mlir/utils.py | 7 +++++++ tests/numpy/test_compile.py | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 9cfae6597..97739a49a 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -79,6 +79,13 @@ def update_bit_width_for_mlir(op_graph: OPGraph): if current_node_out_bit_width > ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB: offending_list.append((node, current_node_out_bit_width)) + # TODO: remove this workaround, which was for #279, once the compiler can handle + # smaller tables, #412 + has_a_table = any(isinstance(node, ArbitraryFunction) for node in op_graph.graph.nodes) + + if has_a_table: + max_bit_width = ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB + _set_all_bit_width(op_graph, max_bit_width) # Check that the max_bit_width is supported by the compiler diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 14be89ef1..3919b6e8f 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -30,6 +30,17 @@ def lut(x): return table[x] +def small_lut(x): + """Test lookup table with small size and output""" + table = LookupTable(list(range(32))) + return table[x] + + +def small_fused_table(x): + """Test with a small fused table""" + return (10 * (numpy.cos(x + 1) + 1)).astype(numpy.uint32) + + @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ @@ -84,6 +95,8 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n pytest.param(lambda x: 8 - x, ((0, 2),), ["x"]), pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), pytest.param(lut, ((0, 127),), ["x"]), + pytest.param(small_lut, ((0, 31),), ["x"]), + pytest.param(small_fused_table, ((0, 31),), ["x"]), ], ) def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_of_arg_names): From b00ab90d12178c43a9a653f7d4d7f954c8aef041 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 20 Sep 2021 09:18:54 +0300 Subject: [PATCH 0274/1104] chore: increase ssh action timeout --- .github/workflows/daily-benchmarks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/daily-benchmarks.yaml b/.github/workflows/daily-benchmarks.yaml index ff9ddf6e5..6101fd637 100644 --- a/.github/workflows/daily-benchmarks.yaml +++ b/.github/workflows/daily-benchmarks.yaml @@ -39,6 +39,7 @@ jobs: host: ${{ steps.public-ip.outputs.value }} username: ${{ secrets.BENCHMARKS_EC2_USERNAME }} key: ${{ secrets.BENCHMARKS_EC2_SSH_KEY }} + command_timeout: 60m script: | cd ~/concretefhe-internal git pull From 0ce47e1895034db08cd45733666936429bef434f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 09:27:27 +0200 Subject: [PATCH 0275/1104] docs: update domain for docs publication --- .github/workflows/continuous-integration.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index f295dd108..859788212 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -233,12 +233,13 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} SOURCE_DIR: '.' + DEST_DIR: 'concretefhe' - name: Invalidate CloudFront Cache if: ${{ steps.publish.outcome == 'success' }} uses: awact/cloudfront-action@8bcfabc7b4bbc0cb8e55e48527f0e3a6d681627c env: - SOURCE_PATH: '/*' + SOURCE_PATH: '/concretefhe/*' AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From eec8e5b9dfc73212b9554840300c1eaca4e7e3ef Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Mon, 20 Sep 2021 09:23:20 +0200 Subject: [PATCH 0276/1104] docs: fix navigation menu colors for mobile --- docs/_static/css/zama.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index 8ae310ebe..70ac7c69e 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -35,3 +35,12 @@ color: #ffd208; background-color: #696969; } + +.wy-nav-top { + color: #696969; + background: #343131 +} + +.wy-nav-top > a { + color: #ffd208; +} From f0b390a090f8d51b59e5a9831478aa84a520e8a9 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 17 Sep 2021 18:45:57 +0200 Subject: [PATCH 0277/1104] fix: more explicit error message closes #251 --- concrete/common/mlir/converters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 20d7703e2..746013839 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -43,7 +43,7 @@ def add(node, preds, ir_to_mlir_node, ctx): ) and value_is_encrypted_scalar_unsigned_integer(node.inputs[1]): return _add_eint_eint(node, preds, ir_to_mlir_node, ctx) raise TypeError( - f"Don't support addition between {type(node.inputs[0])} and {type(node.inputs[1])}" + f"Don't support addition between {str(node.inputs[0])} and {str(node.inputs[1])}" ) @@ -78,7 +78,7 @@ def sub(node, preds, ir_to_mlir_node, ctx): ): return _sub_int_eint(node, preds, ir_to_mlir_node, ctx) raise TypeError( - f"Don't support subtraction between {type(node.inputs[0])} and {type(node.inputs[1])}" + f"Don't support subtraction between {str(node.inputs[0])} and {str(node.inputs[1])}" ) @@ -107,7 +107,7 @@ def mul(node, preds, ir_to_mlir_node, ctx): # flip lhs and rhs return _mul_eint_int(node, preds[::-1], ir_to_mlir_node, ctx) raise TypeError( - f"Don't support multiplication between {type(node.inputs[0])} and {type(node.inputs[1])}" + f"Don't support multiplication between {str(node.inputs[0])} and {str(node.inputs[1])}" ) @@ -174,7 +174,7 @@ def dot(node, preds, ir_to_mlir_node, ctx): ) ): raise TypeError( - f"Don't support dot between {type(node.inputs[0])} and {type(node.inputs[1])}" + f"Don't support dot between {str(node.inputs[0])} and {str(node.inputs[1])}" ) lhs_node, rhs_node = preds # need to flip as underlying operation need encrypted first From 84cb0bdbbe1fb510c8e77a702540069e4bc6aecf Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 17 Sep 2021 12:55:54 +0300 Subject: [PATCH 0278/1104] doc: write compilation artifacts tutorial --- .../artifacts/auto/1.initial.graph.png | Bin 0 -> 7755 bytes .../artifacts/manual/1.initial.graph.png | Bin 0 -> 23985 bytes .../artifacts/manual/2.final.graph.png | Bin 0 -> 23985 bytes docs/index.rst | 1 + docs/user/tutorial/COMPILATION_ARTIFACTS.md | 184 ++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 docs/_static/tutorials/artifacts/auto/1.initial.graph.png create mode 100644 docs/_static/tutorials/artifacts/manual/1.initial.graph.png create mode 100644 docs/_static/tutorials/artifacts/manual/2.final.graph.png create mode 100644 docs/user/tutorial/COMPILATION_ARTIFACTS.md diff --git a/docs/_static/tutorials/artifacts/auto/1.initial.graph.png b/docs/_static/tutorials/artifacts/auto/1.initial.graph.png new file mode 100644 index 0000000000000000000000000000000000000000..e88663dffc60707a81a63d987e12b32008f08ca2 GIT binary patch literal 7755 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 HZyE7Fn!;k- literal 0 HcmV?d00001 diff --git a/docs/_static/tutorials/artifacts/manual/1.initial.graph.png b/docs/_static/tutorials/artifacts/manual/1.initial.graph.png new file mode 100644 index 0000000000000000000000000000000000000000..d4a4f3c97bab43000130355a3de4ddec3263f4d6 GIT binary patch literal 23985 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 +``` + +### 1.initial.graph.txt + +This file contains information about the initial computation graph of the function you are trying to compile. + +``` +%0 = x # EncryptedScalar> +%1 = np.sin(0) # EncryptedScalar> +return(%1) +``` + +### 1.initial.graph.png + +This file contains the visualization of the initial computation graph of the function you are trying to compile. + +![](../../_static/tutorials/artifacts/auto/1.initial.graph.png) + +### traceback.txt + +This file contains information about the error you got. + +``` +Traceback (most recent call last): + File "/src/concrete/numpy/compile.py", line 301, in compile_numpy_function + return _compile_numpy_function_internal( + File "/src/concrete/numpy/compile.py", line 234, in _compile_numpy_function_internal + op_graph = _compile_numpy_function_into_op_graph_internal( + File "/src/concrete/numpy/compile.py", line 103, in _compile_numpy_function_into_op_graph_internal + raise ValueError( +ValueError: cannot be compiled as it has nodes with either float inputs or outputs. +Offending nodes : +``` + +## Manual export + +Manual exports are mostly used for visualization. Nonetheless, they can be very useful for demonstrations. Here is how to do it: + +```python +import concrete.numpy as hnp +import pathlib + +artifacts = hnp.CompilationArtifacts(pathlib.Path("/custom/export/path")) +hnp.compile_numpy_function( + lambda x: 100 - (3 * (x + 2)), + {"x": hnp.EncryptedScalar(hnp.UnsignedInteger(3))}, + inputset=[(i,) for i in range(2 ** 3)], + compilation_artifacts=artifacts, +) +artifacts.export() +``` + +Since this example compiles, we can see some new artifacts. + +### 1.initial.graph.txt + +This file contains information about the initial computation graph of the function you are trying to compile. + +``` +%0 = Constant(100) # ClearScalar> +%1 = Constant(3) # ClearScalar> +%2 = x # EncryptedScalar> +%3 = Constant(2) # ClearScalar> +%4 = Add(2, 3) # EncryptedScalar> +%5 = Mul(4, 1) # EncryptedScalar> +%6 = Sub(0, 5) # EncryptedScalar> +return(%6) +``` + +### 1.initial.graph.png + +This file contains the visualization of the initial computation graph of the function you are trying to compile. + +![](../../_static/tutorials/artifacts/manual/1.initial.graph.png) + +### 2.final.graph.txt + +This file contains information about the final computation graph of the function you are trying to compile. + +``` +%0 = Constant(100) # ClearScalar> +%1 = Constant(3) # ClearScalar> +%2 = x # EncryptedScalar> +%3 = Constant(2) # ClearScalar> +%4 = Add(2, 3) # EncryptedScalar> +%5 = Mul(4, 1) # EncryptedScalar> +%6 = Sub(0, 5) # EncryptedScalar> +return(%6) +``` + +### 2.final.graph.png + +This file contains the visualization of the final computation graph of the function you are trying to compile. + +![](../../_static/tutorials/artifacts/manual/2.final.graph.png) + +### bounds.txt + +This file contains information about the bounds of the final computation graph of the function you are trying to compile using the input set you provide. + +``` +%0 :: [100, 100] +%1 :: [3, 3] +%2 :: [0, 7] +%3 :: [2, 2] +%4 :: [2, 9] +%5 :: [6, 27] +%6 :: [73, 94] +``` + +You can learn what bounds are [here](../../dev/explanation/TERMINOLOGY_AND_STRUCTURE.md). + +### mlir.txt + +This file contains information about the MLIR of the function you are trying to compile using the input set you provide. + +``` +module { + func @main(%arg0: !HLFHE.eint<7>) -> !HLFHE.eint<7> { + %c100_i8 = constant 100 : i8 + %c3_i8 = constant 3 : i8 + %c2_i8 = constant 2 : i8 + %0 = "HLFHE.add_eint_int"(%arg0, %c2_i8) : (!HLFHE.eint<7>, i8) -> !HLFHE.eint<7> + %1 = "HLFHE.mul_eint_int"(%0, %c3_i8) : (!HLFHE.eint<7>, i8) -> !HLFHE.eint<7> + %2 = "HLFHE.sub_int_eint"(%c100_i8, %1) : (i8, !HLFHE.eint<7>) -> !HLFHE.eint<7> + return %2 : !HLFHE.eint<7> + } +} +``` + +You can learn more about MLIR [here](../../dev/explanation/MLIR.md). From 669be9404d2b6b98b8ec88210e330ffde9b4d08c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 10:28:02 +0200 Subject: [PATCH 0279/1104] fix(build): push docs to be publicly readable --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 859788212..06836f44c 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -226,7 +226,7 @@ jobs: if: ${{ steps.download.outcome == 'success' && !cancelled() }} uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 with: - args: --delete + args: --delete --acl public-read env: AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} From 327a85eb974eebbed31b978736f85dc73900ab54 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 17 Sep 2021 12:32:06 +0300 Subject: [PATCH 0280/1104] doc: update the usage of concrete in arithmetic operations tutorial --- docs/user/tutorial/ARITHMETIC_OPERATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md index f9368ff60..32ba1d2ef 100644 --- a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md +++ b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md @@ -1,6 +1,6 @@ # Arithmetic Operations -In this tutorial, we are going to go over all arithmetic operations available in `concretefhe`. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. +In this tutorial, we are going to go over all arithmetic operations available in **concrete**. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. ## Addition From b2b8f5dbae223bce6c4ceead328bb786be43007f Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 17 Sep 2021 12:32:10 +0300 Subject: [PATCH 0281/1104] doc: write table lookup and working with floating points tutorials --- .../table-lookup/1.initial.graph.png | Bin 0 -> 36616 bytes .../tutorials/table-lookup/3.final.graph.png | Bin 0 -> 16902 bytes docs/index.rst | 2 +- docs/user/tutorial/TABLE_LOOKUP.md | 76 +++++++++++++++++- .../tutorial/WORKING_WITH_FLOATING_POINTS.md | 67 ++++++++++++++- 5 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 docs/_static/tutorials/table-lookup/1.initial.graph.png create mode 100644 docs/_static/tutorials/table-lookup/3.final.graph.png diff --git a/docs/_static/tutorials/table-lookup/1.initial.graph.png b/docs/_static/tutorials/table-lookup/1.initial.graph.png new file mode 100644 index 0000000000000000000000000000000000000000..c6dc84e8c36411bc3fa92d5d457e711351825ded GIT binary patch literal 36616 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

bRBhZpp*FfihJZhpb&R!3HSHt+4?IHa9smFU literal 0 HcmV?d00001 diff --git a/docs/_static/tutorials/table-lookup/3.final.graph.png b/docs/_static/tutorials/table-lookup/3.final.graph.png new file mode 100644 index 0000000000000000000000000000000000000000..73b5ab86d4f1ca5d268902e370abcefe8349b25d GIT binary patch literal 16902 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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 15754b2e3..967bb94d7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,8 +14,8 @@ Concrete Framework's documentation :caption: Tutorial user/tutorial/ARITHMETIC_OPERATIONS.md - user/tutorial/WORKING_WITH_FLOATING_POINTS.md user/tutorial/TABLE_LOOKUP.md + user/tutorial/WORKING_WITH_FLOATING_POINTS.md user/tutorial/COMPILATION_ARTIFACTS.md .. toctree:: diff --git a/docs/user/tutorial/TABLE_LOOKUP.md b/docs/user/tutorial/TABLE_LOOKUP.md index 1d92db930..4943e6c86 100644 --- a/docs/user/tutorial/TABLE_LOOKUP.md +++ b/docs/user/tutorial/TABLE_LOOKUP.md @@ -1,3 +1,77 @@ # Table Lookup -Umut to do: #314 +In this tutorial, we are going to go over the ways to perform table lookups in **concrete**. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. + +## Direct table lookup + +**concrete** provides a special class to allow direct table lookups. Here is how to import and use it: + +```python +from concrete.common.extensions.table import LookupTable + +table = LookupTable([2, 1, 3, 0]) + +def f(x): + return table[x] +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(2))` + +results in + +```python +engine.run(0) == 2 +engine.run(1) == 1 +engine.run(2) == 3 +engine.run(3) == 0 +``` + +## Fused table lookup + +Direct tables are tedious to prepare by hand. When possible, **concrete** fuses the floating point operations into a single table lookup automatically. There are some limitations on fusing operations, which you can learn more about on the next tutorial, [Working With Floating Points](./WORKING_WITH_FLOATING_POINTS.md). + +Here is an example function that results in fused table lookup: + +```python +def f(x): + return 127 - (50 * (np.sin(x) + 1)).astype(np.uint32) # astype is to go back to integer world +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(3))` + +results in + +```python +engine.run(0) == 77 +engine.run(1) == 35 +engine.run(2) == 32 +engine.run(3) == 70 +engine.run(4) == 115 +engine.run(5) == 125 +engine.run(6) == 91 +engine.run(7) == 45 +``` + +Initially, the function is converted to this operation graph + +![](../../_static/tutorials/table-lookup/1.initial.graph.png) + +and after floating point operations are fused, we get the following operation graph + +![](../../_static/tutorials/table-lookup/3.final.graph.png) + +Internally, it uses the following lookup table + +```python +table = LookupTable([50, 92, 95, 57, 12, 2, 36, 82]) +``` + +which is calculated by: + +```python +[(50 * (np.sin(x) + 1)).astype(np.uint32) for x in range(2 ** 3)] +``` diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md index 4b396c886..60ee51ad0 100644 --- a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -1,3 +1,68 @@ # Working With Floating Points -Umut to do: #313 +## An example + +```python +def f(x): + np.fabs(100 * (2 * np.sin(x) * np.cos(x))).astype(np.uint32) # astype is to go back to integer world +``` + +where + +- `x = EncryptedScalar(UnsignedInteger(bits))` + +results in + +```python +engine.run(3) == 27 +engine.run(0) == 0 +engine.run(1) == 90 +engine.run(10) == 91 +engine.run(60) == 58 +``` + +## Supported operations + +The following operations are supported in the latest release, and we'll add more operations in the upcoming releases. + +- np.arccos +- np.arccosh +- np.arcsin +- np.arcsinh +- np.arctan +- np.arctanh +- np.cbrt +- np.ceil +- np.cos +- np.cosh +- np.deg2rad +- np.degrees +- np.exp +- np.exp2 +- np.expm1 +- np.fabs +- np.floor +- np.log +- np.log10 +- np.log1p +- np.log2 +- np.rad2deg +- np.radians +- np.rint +- np.sin +- np.sinh +- np.spacing +- np.sqrt +- np.tan +- np.tanh +- np.trunc + +## Limitations + +Floating point support in **concrete** is very limited for the time being. They can't appear on inputs, or they can't be outputs. However, they can be used in intermediate results. Unfortunately, there are limitations on that front as well. + +This biggest one is that, because floating point operations are fused into table lookups with a single unsigned integer input and single unsigned integer output, only univariate portion of code can be replaced with table lookups, which means multivariate portions cannot be compiled. + +To give a precise example, `100 - np.fabs(50 * (np.sin(x) + np.sin(y)))` cannot be compiled because the floating point part depends on both `x` and `y` (i.e., it cannot be rewritten in the form `100 - table[z]` for a `z` that could be computed easily from `x` and `y`). + +To dive into implementation details, you may refer to [Fusing Floating Point Operations](../../dev/explanation/FLOAT-FUSING.md) document. From a48ed222bb9397c08e411c50fadd37aa61ee5242 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 12:02:28 +0200 Subject: [PATCH 0282/1104] build: add url to action run in slack notification --- .github/workflows/continuous-integration.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 06836f44c..6ebe3db23 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -25,6 +25,7 @@ env: PREFLIGHT_IMAGE_BASE: ghcr.io/zama-ai/concretefhe-env:preflight LATEST_IMAGE: ghcr.io/zama-ai/concretefhe-env:latest BASE_IMAGE: ghcr.io/zama-ai/concretefhe-env + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} jobs: build_preflight_docker: @@ -96,7 +97,8 @@ jobs: SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: "Docker image preflight build ${{ env.PREFLIGHT_IMAGE }} finished with \ - status ${{ job.status }}. Rebuilt image: ${{ env.BUILD_DOCKER || 'false' }}." + status ${{ job.status }}. Rebuilt image: ${{ env.BUILD_DOCKER || 'false' }}. \ + (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -201,7 +203,7 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: 'Build finished with status ${{ job.status }}' + SLACK_MESSAGE: "Build finished with status ${{ job.status }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -253,7 +255,8 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: 'Publishing documentation finished with status ${{ job.status }}' + SLACK_MESSAGE: "Publishing documentation finished with status ${{ job.status }} \ + (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -300,6 +303,6 @@ jobs: SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} SLACK_MESSAGE: "Publishing docker image ${{ env.BASE_IMAGE }} finished with status \ - ${{ job.status }}" + ${{ job.status }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From e957f63b812ea2d2904776a97b124328c0b20043 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 20 Sep 2021 11:58:24 +0200 Subject: [PATCH 0283/1104] doc: add a benchmarks section closes #423 --- docs/benchmarks.md | 18 ++++++++++++++++++ docs/index.rst | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 docs/benchmarks.md diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 000000000..867d652e2 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,18 @@ +# Benchmarks + +In order to track our progress over time, we have set a [public benchmark](https://progress.zama.ai) containing: +- a list of functions that we want to compile +- status on the compilation of these functions +- compilation time +- evaluation time on different hardware's +- accuracy of the functions for which it makes sense +- loss of the functions for which it makes sense + +Remark that we are not limited to these, and we'll certainly add more information later, as key generation time, encryption and decryption time, and more evaluation time once the explicit inference API is available. + +The benchmark can be used by competitive frameworks or technologies, in order to compare fairly with the **Concrete Framework**. Notably, one can see: +- if the same functions can be compiled +- what are discrepancies in the exactness of the evaluations +- how do evaluation times compare + +If one wants to see more functions in the benchmark or if there is more information you would like the benchmark to track, don't hesitate to drop an email to . \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 967bb94d7..e75fb5d81 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,10 +4,11 @@ Concrete Framework's documentation .. toctree:: :maxdepth: 2 - :caption: Basics + :caption: Getting Started README.md user/howto/INSTALLING.md + benchmarks.md .. toctree:: :maxdepth: 2 From dc5446d5bf6b915af9ba7728a11309c46321706b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 13:54:02 +0200 Subject: [PATCH 0284/1104] fix(tools): force reinstall when installing setuptools - prevents a poetry bug from breaking the package --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 197cca3ee..2c4a88e83 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ NOTEBOOKS_DIR:=docs/user/advanced_examples setup_env: poetry install - poetry run python -m pip install -U pip wheel setuptools + poetry run python -m pip install -U pip wheel + poetry run python -m pip install -U --force-reinstall setuptools poetry run python -m pip install -r torch_requirements.txt \ -f https://download.pytorch.org/whl/torch_stable.html .PHONY: setup_env From d546e17c1fa9d3245d3e52e77794db0f4bf32dfa Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 14:15:42 +0200 Subject: [PATCH 0285/1104] fix(build): do not use actions for incompatible events --- .github/workflows/continuous-integration.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 6ebe3db23..75351c0f4 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -43,6 +43,7 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Get changed files + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} uses: Ana06/get-changed-files@ea75ed777daf24d6e95f43957bd26b1eab20806c id: files with: From f4d7cab3593e3bedce340c31886b0f74b013d16b Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 20 Sep 2021 11:41:03 +0200 Subject: [PATCH 0286/1104] doc: add a 'future feature' section closes #321 --- docs/user/explanation/FUTURE_FEATURES.md | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/user/explanation/FUTURE_FEATURES.md b/docs/user/explanation/FUTURE_FEATURES.md index a66f11b3f..8e608766e 100644 --- a/docs/user/explanation/FUTURE_FEATURES.md +++ b/docs/user/explanation/FUTURE_FEATURES.md @@ -1,3 +1,27 @@ # Future Features -Alex to do: #321 +As explained in [this section](FHE_AND_FRAMEWORK_LIMITS.md#concrete-framework-limits), the **Concrete Framework** +is currently in a preliminary version, and quite constrained in term of functionalities. However, the good +news is that we are going to release new versions regularly, where a lot of functionalities will be added progressively. + +In this page, we briefly list what the plans for next versions of the **Concrete Framework** are: +- **management of tensors**: today, we are mostly limited to scalars, but in the next version, the functions we compile +will possibly contain tensors, which is one of the basic features of `numpy` +- **better performance**: further versions will contain improved versions of the **Concrete Library**, with faster +execution; also, the **Concrete Compiler** will be improved, to have faster local execution (with multi-threading +for example) and faster production execution (with distribution over a set of machines or use of hardware accelerations) +- **more user-friendly API's**: we would like to make our API easier for a user. Notably, we would like to allow direct +compilations of classic Machine Learning framework models (e.g., tensorflow or pytorch) +- **more complete benchmarks**: we will have an extended benchmark, containing lots of functions that one day one would +want to compile; then, we will measure the framework progress by tracking the number of successfully compiled functions +over time. Also, this public benchmark will be a way for other competing frameworks or technologies to compare fairly +with us, in terms of functionality or performance +- **easier installation**: we plan to have pip installation of our framework very soon +- **Machine Learning helpers**: our midterm direction is to provide our users a set of tools to help her turn her use case +in an homomorphic equivalent. This set of tools will help her reduce the needed variable precision and/or optimize the +operations required to make the fastest possible compiled model. + +Also, if you are especially looking for some new feature, you can drop a message to . + + + From 441c4f9e7d9c714a0bdd0393826fcf19ee7a9313 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 20 Sep 2021 12:35:46 +0300 Subject: [PATCH 0287/1104] feat: check inputset size --- .../bounds_measurement/inputset_eval.py | 13 ++++++--- concrete/common/mlir/utils.py | 5 +++- concrete/numpy/compile.py | 27 ++++++++++++++++++- docs/user/howto/COMPILING_AND_EXECUTING.md | 2 +- .../bounds_measurement/test_inputset_eval.py | 2 +- tests/common/compilation/test_artifacts.py | 2 +- .../common/compilation/test_configuration.py | 4 +-- tests/common/debugging/test_drawing.py | 2 +- tests/common/mlir/test_mlir_converter.py | 12 ++++----- tests/numpy/test_compile.py | 24 ++++++++++++----- 10 files changed, 68 insertions(+), 25 deletions(-) diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index 8f83e02fc..f285de2be 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -12,7 +12,7 @@ def eval_op_graph_bounds_on_inputset( inputset: Iterable[Tuple[Any, ...]], min_func: Callable[[Any, Any], Any] = min, max_func: Callable[[Any, Any], Any] = max, -) -> Dict[IntermediateNode, Dict[str, Any]]: +) -> Tuple[int, Dict[IntermediateNode, Dict[str, Any]]]: """Evaluate the bounds with a inputset. Evaluate the bounds for all output values of the operators in the graph op_graph over data @@ -31,8 +31,9 @@ def eval_op_graph_bounds_on_inputset( tensors). Defaults to max. Returns: - Dict[IntermediateNode, Dict[str, Any]]: dict containing the bounds for each node from - op_graph, stored with the node as key and a dict with keys "min" and "max" as value. + Tuple[int, Dict[IntermediateNode, Dict[str, Any]]]: number of inputs in the inputset and + a dict containing the bounds for each node from op_graph, stored with the node + as key and a dict with keys "min" and "max" as value. """ def check_inputset_input_len_is_valid(data_to_check): @@ -48,9 +49,12 @@ def eval_op_graph_bounds_on_inputset( # TODO: do we want to check coherence between the input data type and the corresponding Input ir # node expected data type ? Not considering bit_width as they may not make sense at this stage + inputset_size = 0 inputset_iterator = iter(inputset) first_input_data = dict(enumerate(next(inputset_iterator))) + inputset_size += 1 + check_inputset_input_len_is_valid(first_input_data.values()) first_output = op_graph.evaluate(first_input_data) @@ -62,6 +66,7 @@ def eval_op_graph_bounds_on_inputset( } for input_data in inputset_iterator: + inputset_size += 1 current_input_data = dict(enumerate(input_data)) check_inputset_input_len_is_valid(current_input_data.values()) current_output = op_graph.evaluate(current_input_data) @@ -69,4 +74,4 @@ def eval_op_graph_bounds_on_inputset( node_bounds[node]["min"] = min_func(node_bounds[node]["min"], value) node_bounds[node]["max"] = max_func(node_bounds[node]["max"], value) - return node_bounds + return inputset_size, node_bounds diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 97739a49a..3b90d349e 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -108,7 +108,10 @@ def extend_direct_lookup_tables(op_graph: OPGraph): table = node.op_kwargs["table"] bit_width = cast(Integer, node.inputs[0].data_type).bit_width expected_length = 2 ** bit_width - if len(table) > expected_length: + + # TODO: remove no cover once the table length workaround is removed + # (https://github.com/zama-ai/concretefhe-internal/issues/359) + if len(table) > expected_length: # pragma: no cover node.op_kwargs["table"] = table[:expected_length] else: repeat = expected_length // len(table) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 03a894464..ea4d3925e 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -1,5 +1,6 @@ """numpy compilation function.""" +import sys import traceback from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple @@ -9,6 +10,7 @@ from zamalang import CompilerEngine from ..common.bounds_measurement.inputset_eval import eval_op_graph_bounds_on_inputset from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts, CompilationConfiguration +from ..common.data_types import Integer from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( extend_direct_lookup_tables, @@ -107,13 +109,36 @@ def _compile_numpy_function_into_op_graph_internal( ) # Find bounds with the inputset - node_bounds = eval_op_graph_bounds_on_inputset( + inputset_size, node_bounds = eval_op_graph_bounds_on_inputset( op_graph, inputset, min_func=numpy_min_func, max_func=numpy_max_func, ) + # Check inputset size + inputset_size_upper_limit = 1 + + # this loop will determine the number of possible inputs of the function + # if a function have a single 3-bit input, for example, `inputset_size_upper_limit` will be 8 + for parameter_value in function_parameters.values(): + if isinstance(parameter_value.data_type, Integer): + # multiple parameter bit-widths are multiplied as they can be combined into an input + inputset_size_upper_limit *= 2 ** parameter_value.data_type.bit_width + + # if the upper limit of the inputset size goes above 10, + # break the loop as we will require at least 10 inputs in this case + if inputset_size_upper_limit > 10: + break + + minimum_required_inputset_size = min(inputset_size_upper_limit, 10) + if inputset_size < minimum_required_inputset_size: + sys.stderr.write( + f"Provided inputset contains too few inputs " + f"(it should have had at least {minimum_required_inputset_size} " + f"but it only had {inputset_size})\n" + ) + # Add the bounds as an artifact compilation_artifacts.add_final_operation_graph_bounds(node_bounds) diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index 42abed017..21866991a 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -28,7 +28,7 @@ y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) In this configuration, both `x` and `y` are 3-bit unsigned integers, so they have the range of `[0, 2**3 - 1]` -We also need an inputset. This latter is not to be confused with the dataset, which is used in training and contains labels. It is to determine the bit-widths of the intermediate results so only the inputs are necessary. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. +We also need an inputset. It is to determine the bit-widths of the intermediate results. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. There should be at least 10 inputs in the input set to avoid warnings (except for functions with less than 10 possible inputs). The warning is there because the bigger the input set, the better the bounds will be. ```python inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index 8fa937c22..d2b07f30a 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -281,7 +281,7 @@ def test_eval_op_graph_bounds_on_inputset_multiple_output( for y_gen in range_y: yield (x_gen, y_gen) - node_bounds = eval_op_graph_bounds_on_inputset( + _, node_bounds = eval_op_graph_bounds_on_inputset( op_graph, data_gen(*tuple(range(x[0], x[1] + 1) for x in input_ranges)) ) diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 4ff9b9c15..59230c92d 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -22,7 +22,7 @@ def test_artifacts_export(): compile_numpy_function( function, {"x": EncryptedScalar(UnsignedInteger(7))}, - [(0,), (1,), (2,)], + [(i,) for i in range(10)], compilation_artifacts=artifacts, ) diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index fde2131bd..24ca8db5d 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -49,7 +49,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused param: EncryptedScalar(Integer(32, is_signed=False)) for param in signature(function_to_trace).parameters.keys() }, - [(1,), (2,), (3,)], + [(i,) for i in range(10)], CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) op_graph_not_optimized = compile_numpy_function_into_op_graph( @@ -58,7 +58,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused param: EncryptedScalar(Integer(32, is_signed=False)) for param in signature(function_to_trace).parameters.keys() }, - [(1,), (2,), (3,)], + [(i,) for i in range(10)], CompilationConfiguration( dump_artifacts_on_unexpected_failures=False, enable_topological_optimizations=False, diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py index c8396e619..7d5fced48 100644 --- a/tests/common/debugging/test_drawing.py +++ b/tests/common/debugging/test_drawing.py @@ -18,7 +18,7 @@ def test_draw_graph_with_saving(): op_graph = compile_numpy_function_into_op_graph( function, {"x": EncryptedScalar(Integer(7, True))}, - [(-2,), (-1,), (0,), (1,), (2,)], + [(i,) for i in range(-5, 5)], ) with tempfile.TemporaryDirectory() as tmp: diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 5769dffda..fec21c6e1 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -33,7 +33,7 @@ def sub(x, y): def constant_sub(x): """Test constant sub""" - return 8 - x + return 12 - x def mul(x, y): @@ -108,7 +108,7 @@ def datagen(*args): { "x": EncryptedScalar(Integer(64, is_signed=False)), }, - (range(0, 8),), + (range(0, 10),), ), ( add, @@ -139,7 +139,7 @@ def datagen(*args): { "x": EncryptedScalar(Integer(64, is_signed=False)), }, - (range(0, 5),), + (range(0, 10),), ), ( mul, @@ -154,7 +154,7 @@ def datagen(*args): { "x": EncryptedScalar(Integer(64, is_signed=False)), }, - (range(0, 8),), + (range(0, 10),), ), ( mul, @@ -194,7 +194,7 @@ def datagen(*args): ( lut, { - "x": EncryptedScalar(Integer(64, is_signed=False)), + "x": EncryptedScalar(Integer(3, is_signed=False)), }, (range(0, 8),), ), @@ -209,7 +209,7 @@ def datagen(*args): ( lut_less_bits_than_table_length, { - "x": EncryptedScalar(Integer(64, is_signed=False)), + "x": EncryptedScalar(Integer(3, is_signed=False)), }, (range(0, 8),), ), diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 3919b6e8f..be4e47432 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -44,9 +44,9 @@ def small_fused_table(x): @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ - pytest.param(lambda x: x + 42, ((-2, 2),), ["x"]), + pytest.param(lambda x: x + 42, ((-5, 5),), ["x"]), pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), - pytest.param(lambda x, y: (x + 1, y + 10), ((-1, 1), (3, 4)), ["x", "y"]), + pytest.param(lambda x, y: (x + 1, y + 10), ((-1, 1), (3, 8)), ["x", "y"]), pytest.param( lambda x, y, z: (x + y + 1 - z, x * y + 42, z, z + 99), ((4, 8), (3, 4), (0, 4)), @@ -89,10 +89,10 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ - pytest.param(lambda x: x + 42, ((0, 2),), ["x"]), - pytest.param(lambda x: x + numpy.int32(42), ((0, 2),), ["x"]), - pytest.param(lambda x: x * 2, ((0, 2),), ["x"]), - pytest.param(lambda x: 8 - x, ((0, 2),), ["x"]), + pytest.param(lambda x: x + 42, ((0, 10),), ["x"]), + pytest.param(lambda x: x + numpy.int32(42), ((0, 10),), ["x"]), + pytest.param(lambda x: x * 2, ((0, 10),), ["x"]), + pytest.param(lambda x: 12 - x, ((0, 10),), ["x"]), pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), pytest.param(lut, ((0, 127),), ["x"]), pytest.param(small_lut, ((0, 31),), ["x"]), @@ -240,7 +240,7 @@ def test_compile_function_with_direct_tlu_overflow(): @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ - pytest.param(lambda x: x - 10, ((-2, 2),), ["x"]), + pytest.param(lambda x: x - 10, ((-5, 5),), ["x"]), ], ) def test_fail_compile(function, input_ranges, list_of_arg_names): @@ -263,6 +263,16 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): ) +def test_small_inputset(): + """Test function compile_numpy_function_into_op_graph with an unacceptably small inputset""" + compile_numpy_function_into_op_graph( + lambda x: x + 42, + {"x": EncryptedScalar(Integer(5, is_signed=False))}, + [(0,), (3,)], + CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), + ) + + @pytest.mark.parametrize( "function,params,shape,ref_graph_str", [ From fbec6e7235e9af5c8233176056decb3cfa9c11e1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 11:54:49 +0200 Subject: [PATCH 0288/1104] fix(artifacts): mkdir with parents --- concrete/common/compilation/artifacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index eaefd8f3f..c1af6be45 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -128,7 +128,7 @@ class CompilationArtifacts: output_directory = self.output_directory if output_directory.exists(): shutil.rmtree(output_directory) - output_directory.mkdir() + output_directory.mkdir(parents=True) with open(output_directory.joinpath("environment.txt"), "w", encoding="utf-8") as f: f.write(f"{platform.platform()} {platform.version()}\n") From 5aa4ce1ef8ab82bd93c4753543d01588880f36b8 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 11:46:34 +0200 Subject: [PATCH 0289/1104] fix(build): test coverage in pytest --- .github/workflows/continuous-integration.yaml | 2 +- Makefile | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 75351c0f4..1f908762e 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -181,7 +181,7 @@ jobs: make pytest_nb - name: Test coverage id: coverage - if: ${{ steps.pytest.outcome != 'skipped' && !cancelled() }} + if: ${{ always() && steps.pytest.outcome != 'skipped' && !cancelled() }} run: | ./script/actions_utils/coverage.sh ${{ github.base_ref }} - name: Archive test coverage diff --git a/Makefile b/Makefile index 2c4a88e83..06c70be81 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,8 @@ pcc_internal: check_python_format check_finalize_nb python_linting mypy_ci pydoc pytest: poetry run pytest -svv \ - --cov=$(SRC_DIR) --cov-report=term-missing:skip-covered --cov-report=xml tests/ + --cov=$(SRC_DIR) --cov-fail-under=100 \ + --cov-report=term-missing:skip-covered --cov-report=xml tests/ .PHONY: pytest # Not a huge fan of ignoring missing imports, but some packages do not have typing stubs From 35011bd4031ee58269c435f8d972fd7b9c5dbca1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 10:27:23 +0200 Subject: [PATCH 0290/1104] build: test codeblocks in CI --- .github/workflows/continuous-integration.yaml | 11 +- Makefile | 6 + docs/dev/explanation/COMPILATION.md | 2 +- docs/dev/explanation/FLOAT-FUSING.md | 3 + docs/user/howto/COMPILING_AND_EXECUTING.md | 21 ++- docs/user/tutorial/ARITHMETIC_OPERATIONS.md | 23 +++ docs/user/tutorial/COMPILATION_ARTIFACTS.md | 3 +- docs/user/tutorial/TABLE_LOOKUP.md | 5 + .../tutorial/WORKING_WITH_FLOATING_POINTS.md | 2 + script/make_utils/test_md_python_code.py | 140 ++++++++++++++++++ 10 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 script/make_utils/test_md_python_code.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 1f908762e..bc1cbe91d 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -164,7 +164,7 @@ jobs: with: name: html-docs path: docs/_build/html - - name: PyTest + - name: PyTest Source Code id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} env: @@ -172,7 +172,14 @@ jobs: LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make pytest - - name: Notebooks + - name: Test CodeBlocks + if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} + env: + # TODO: remove this when JIT doesn't need this + LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so + run: | + make test_codeblocks + - name: PyTest Notebooks if: ${{ github.event_name == 'schedule' && steps.conformance.outcome == 'success' && !cancelled() }} env: # TODO: remove this when JIT doesn't need this diff --git a/Makefile b/Makefile index 06c70be81..1202ad180 100644 --- a/Makefile +++ b/Makefile @@ -197,3 +197,9 @@ release_docker: upgrade_py_deps: ./script/make_utils/upgrade_deps.sh .PHONY: upgrade_py_deps + +# This is done by hand as pytest-codeblocks was failing with our native extensions. +# See refused PR on the project here: https://github.com/nschloe/pytest-codeblocks/pull/58 +test_codeblocks: + poetry run python ./script/make_utils/test_md_python_code.py --md_dir docs/ +.PHONY: test_codeblocks diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index 253edd7e8..cf6f08f8e 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -28,7 +28,7 @@ engine = hnp.compile_numpy_function( ) # Make homomorphic inference -engine.run([1, 0]) +engine.run(1, 0) ``` ## Overview diff --git a/docs/dev/explanation/FLOAT-FUSING.md b/docs/dev/explanation/FLOAT-FUSING.md index c283246a4..ac10088d8 100644 --- a/docs/dev/explanation/FLOAT-FUSING.md +++ b/docs/dev/explanation/FLOAT-FUSING.md @@ -6,6 +6,7 @@ The current compiler stack only supports integers with 7 bits or less. But it's We added fusing floating point operations to make tracing numpy functions somewhat user friendly to allow in-line quantization in the numpy code e.g.: + ```python import numpy @@ -42,6 +43,7 @@ From the terminal node, we go back up through the nodes until we find nodes that An example of a non fusable computation with that technique is: + ```python import numpy @@ -63,6 +65,7 @@ Firstly, it does not cover optimizing the graph, so you can end up with multiple Secondly, the current approach fails to handle some programs that in practice could be compiled. The following example could be covered by pushing the search to find a single integer input: + ```python def theoretically_fusable(x): x_1 = x + 1.5 diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index 21866991a..6c5809779 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -12,6 +12,7 @@ import concrete.numpy as hnp You need to have a python function that follows the [limits](../explanation/FHE_AND_FRAMEWORK_LIMITS.md) of the **Concrete Framework**. Here is a simple example: + ```python def f(x, y): return x + y @@ -21,6 +22,7 @@ def f(x, y): To compile the function, you need to provide what are the inputs that it's expecting. In the example function above, `x` and `y` could be scalars or tensors (though, for now, only dot between tensors are supported), they can be encrypted or clear, they can be signed or unsigned, they can have different bit-widths. So, we need to know what they are beforehand. We can do that like so: + ```python x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) @@ -30,12 +32,14 @@ In this configuration, both `x` and `y` are 3-bit unsigned integers, so they hav We also need an inputset. It is to determine the bit-widths of the intermediate results. It should be an iterable yielding tuples in the same order as the inputs of the function to compile. There should be at least 10 inputs in the input set to avoid warnings (except for functions with less than 10 possible inputs). The warning is there because the bigger the input set, the better the bounds will be. + ```python inputset = [(2, 3), (0, 0), (1, 6), (7, 7), (7, 1)] ``` Finally, we can compile our function to its homomorphic equivalent. + ```python engine = hnp.compile_numpy_function( f, {"x": x, "y": y}, @@ -47,15 +51,16 @@ engine = hnp.compile_numpy_function( You can use `.run(...)` method of `engine` returned by `hnp.compile_numpy_function(...)` to perform fully homomorphic evaluation. Here are some examples: + ```python ->>> engine.run(3, 4) -7 ->>> engine.run(1, 2) -3 ->>> engine.run(7, 7) -14 ->>> engine.run(0, 0) -0 +engine.run(3, 4) +# 7 +engine.run(1, 2) +# 3 +engine.run(7, 7) +# 14 +engine.run(0, 0) +# 0 ``` ```{caution} diff --git a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md index 32ba1d2ef..14ab6cb94 100644 --- a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md +++ b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md @@ -6,6 +6,7 @@ In this tutorial, we are going to go over all arithmetic operations available in ### Static ClearScalar and EncryptedScalar + ```python def f(x): return x + 42 @@ -13,6 +14,7 @@ def f(x): or + ```python def f(x): return 42 + x @@ -24,6 +26,7 @@ where results in + ```python engine.run(3) == 45 engine.run(0) == 42 @@ -31,6 +34,7 @@ engine.run(0) == 42 ### Dynamic ClearScalar and EncryptedScalar + ```python def f(x, y): return x + y @@ -38,6 +42,7 @@ def f(x, y): or + ```python def f(x, y): return y + x @@ -45,6 +50,7 @@ def f(x, y): results in + ```python engine.run(6, 4) == 10 engine.run(1, 1) == 2 @@ -57,6 +63,7 @@ where ### EncryptedScalar and EncryptedScalar + ```python def f(x, y): return x + y @@ -69,6 +76,7 @@ where results in + ```python engine.run(7, 7) == 14 engine.run(3, 4) == 7 @@ -78,6 +86,7 @@ engine.run(3, 4) == 7 ### Static ClearScalar and EncryptedScalar + ```python def f(x): return 3 - x @@ -89,6 +98,7 @@ where results in + ```python engine.run(2) == 1 engine.run(3) == 0 @@ -96,6 +106,7 @@ engine.run(3) == 0 ### Dynamic ClearScalar and EncryptedScalar + ```python def f(x, y): return y - x @@ -108,6 +119,7 @@ where results in + ```python engine.run(2, 4) == 2 engine.run(1, 7) == 6 @@ -117,6 +129,7 @@ engine.run(1, 7) == 6 ### Static ClearScalar and EncryptedScalar + ```python def f(x): return x * 2 @@ -124,6 +137,7 @@ def f(x): or + ```python def f(x): return 2 * x @@ -135,6 +149,7 @@ where results in + ```python engine.run(2) == 4 engine.run(5) == 10 @@ -142,6 +157,7 @@ engine.run(5) == 10 ### Dynamic ClearScalar and EncryptedScalar + ```python def f(x, y): return x * y @@ -149,6 +165,7 @@ def f(x, y): or + ```python def f(x, y): return y * x @@ -161,6 +178,7 @@ where results in + ```python engine.run(2, 3) == 6 engine.run(1, 7) == 7 @@ -170,6 +188,7 @@ engine.run(1, 7) == 7 ### Dynamic ClearTensor and EncryptedTensor + ```python def f(x, y): return np.dot(x, y) @@ -177,6 +196,7 @@ def f(x, y): or + ```python def f(x, y): return np.dot(y, x) @@ -189,6 +209,7 @@ where results in + ```python engine.run([1, 1], [2, 3]) == 5 engine.run([2, 3], [2, 3]) == 13 @@ -196,6 +217,7 @@ engine.run([2, 3], [2, 3]) == 13 ## Combining all together + ```python def f(x, y, z): return 100 - (2 * (np.dot(x, y) + z)) @@ -209,6 +231,7 @@ where results in + ```python engine.run([1, 2], [4, 3], 10) == 60 engine.run([2, 3], [3, 2], 5) == 66 diff --git a/docs/user/tutorial/COMPILATION_ARTIFACTS.md b/docs/user/tutorial/COMPILATION_ARTIFACTS.md index 2b08811e6..576339e91 100644 --- a/docs/user/tutorial/COMPILATION_ARTIFACTS.md +++ b/docs/user/tutorial/COMPILATION_ARTIFACTS.md @@ -6,6 +6,7 @@ In this tutorial, we are going to go over the artifact system, which is designed In case of compilation failures, artifacts are exported automatically to `.artifacts` directory under the working directory. Let's intentionally create a compilation failure and show what kinds of things are exported. + ```python def f(x): return np.sin(x) @@ -93,7 +94,7 @@ Manual exports are mostly used for visualization. Nonetheless, they can be very import concrete.numpy as hnp import pathlib -artifacts = hnp.CompilationArtifacts(pathlib.Path("/custom/export/path")) +artifacts = hnp.CompilationArtifacts(pathlib.Path("/tmp/custom/export/path")) hnp.compile_numpy_function( lambda x: 100 - (3 * (x + 2)), {"x": hnp.EncryptedScalar(hnp.UnsignedInteger(3))}, diff --git a/docs/user/tutorial/TABLE_LOOKUP.md b/docs/user/tutorial/TABLE_LOOKUP.md index 4943e6c86..d2bb54d9c 100644 --- a/docs/user/tutorial/TABLE_LOOKUP.md +++ b/docs/user/tutorial/TABLE_LOOKUP.md @@ -21,6 +21,7 @@ where results in + ```python engine.run(0) == 2 engine.run(1) == 1 @@ -34,6 +35,7 @@ Direct tables are tedious to prepare by hand. When possible, **concrete** fuses Here is an example function that results in fused table lookup: + ```python def f(x): return 127 - (50 * (np.sin(x) + 1)).astype(np.uint32) # astype is to go back to integer world @@ -45,6 +47,7 @@ where results in + ```python engine.run(0) == 77 engine.run(1) == 35 @@ -66,12 +69,14 @@ and after floating point operations are fused, we get the following operation gr Internally, it uses the following lookup table + ```python table = LookupTable([50, 92, 95, 57, 12, 2, 36, 82]) ``` which is calculated by: + ```python [(50 * (np.sin(x) + 1)).astype(np.uint32) for x in range(2 ** 3)] ``` diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md index 60ee51ad0..0fe234602 100644 --- a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -2,6 +2,7 @@ ## An example + ```python def f(x): np.fabs(100 * (2 * np.sin(x) * np.cos(x))).astype(np.uint32) # astype is to go back to integer world @@ -13,6 +14,7 @@ where results in + ```python engine.run(3) == 27 engine.run(0) == 0 diff --git a/script/make_utils/test_md_python_code.py b/script/make_utils/test_md_python_code.py new file mode 100644 index 000000000..71660cc2e --- /dev/null +++ b/script/make_utils/test_md_python_code.py @@ -0,0 +1,140 @@ +"""Helper script to be able to test python code in markdown files.""" + +import argparse +import re +import sys +import traceback +from pathlib import Path +from typing import Dict, List + +PYTHON_BLOCK_HINTS = ["py", "python", "python3"] +BLOCK_STARTS = tuple(f"```{hint}" for hint in PYTHON_BLOCK_HINTS) +BLOCK_END = "```" +DIRECTIVE_COMMENT_PATTERN = "" +SKIP_DIRECTIVE = "skip" +CONT_DIRECTIVE = "cont" + + +def get_code_blocks_for_file(md_file: Path) -> Dict[int, List[str]]: + """Function to process an md file and test the python code in it. + + Args: + md_file (Path): The path to the md file to convert and test. + + Raises: + SyntaxError: If EOF is reached before a code block is closed. + SyntaxError: If a block is not closed and a new python block is opened. + + Returns: + Dict[int, List[str]]: A dict containing the code blocks of the file. + """ + file_content = None + + python_code_blocks: Dict[int, List[str]] = {} + + def get_code_block_container(line_idx): + block_idx = line_idx + python_code_blocks[block_idx] = [] + return python_code_blocks[block_idx] + + with open(md_file, encoding="utf-8") as f: + file_content = f.readlines() + + file_content_iterator = iter(enumerate(file_content, 1)) + python_block_continues = False + skip_next_python_block = False + + for line_idx, line in file_content_iterator: + if line.startswith(BLOCK_STARTS): + if skip_next_python_block: + skip_next_python_block = False + continue + if not python_block_continues: + current_python_code = get_code_block_container(line_idx) + while True: + line_idx, line = next(file_content_iterator) + if line == "": + # Reached EOF + raise SyntaxError( + "Reached EOF before finding the end of the current python block in " + f"{str(md_file)}" + ) + + if line.strip() == BLOCK_END: + break + + if line.startswith(BLOCK_STARTS): + raise SyntaxError( + f"Error at line {line_idx} in file {str(md_file)}, " + "python block was opened before the previous one was " + "closed (missing ``` ?)" + ) + current_python_code.append(line) + else: + match = re.match(DIRECTIVE_COMMENT_PATTERN, line) + if match is not None: + directive = match.group(1) + if directive == SKIP_DIRECTIVE: + skip_next_python_block = True + elif directive == CONT_DIRECTIVE: + python_block_continues = True + + python_block_continues = python_block_continues and not skip_next_python_block + + return python_code_blocks + + +def main(args): + """The actual processing.""" + md_dir_path = Path(args.md_dir) + md_files = sorted(md_dir_path.glob("**/*.md")) + + code_blocks_per_file: Dict[str, Dict[int, List[str]]] = {} + + err_msg = "" + + for md_file in md_files: + md_file = md_file.resolve().absolute() + md_file_str = str(md_file) + # pylint: disable=broad-except + try: + code_blocks_per_file[md_file_str] = get_code_blocks_for_file(md_file) + except Exception: + err_msg += f"Error while converting {md_file_str}" + err_msg += traceback.format_exc() + "\n" + # pylint: enable=broad-except + + for md_file_str, code_blocks in code_blocks_per_file.items(): + for line_idx, python_code in code_blocks.items(): + # pylint: disable=broad-except,exec-used + try: + print(f"Testing block starting line #{line_idx} from {md_file_str}") + python_code = "".join(python_code) + compiled_code = compile(python_code, filename=md_file_str, mode="exec") + exec(compiled_code, {"__MODULE__": "__main__"}) + print("Success") + except Exception: + print("Failed") + err_msg += ( + f"Error while testing block starting line #{line_idx} from {md_file_str}:\n" + ) + err_msg += f"```\n{python_code}```\n" + err_msg += traceback.format_exc() + "\n" + # pylint: enable=broad-except,exec-used + + if err_msg != "": + print(err_msg) + sys.exit(1) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + "Converts md python blocks to python files", allow_abbrev=False + ) + parser.add_argument( + "--md_dir", type=str, help="The path to the dir containing md files to convert." + ) + + cli_args = parser.parse_args() + + main(cli_args) From 10cbfa1652614b9cc7f3f5fcd3d8a08138c93d41 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 17:23:02 +0200 Subject: [PATCH 0291/1104] build: automate a part of the release process --- .github/ISSUE_TEMPLATE/release.md | 8 +- .github/workflows/continuous-integration.yaml | 78 +++++++++++++++++-- docker/release_resources/sanity_check.py | 36 +++++++++ 3 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 docker/release_resources/sanity_check.py diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 9f456f1b1..9224e76c4 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -10,11 +10,7 @@ Release check-list: - [ ] Update the version in pyproject.toml to `X.Y.Z` (or `X.Y.Zrc?`) - [ ] Check the release milestone issues, cut out what can't be completed in time - [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Zrc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Zrc?`) -- [ ] Run sanity checks inside the dev docker: `make docker_build_and_start`, `make pcc` and `make pytest && make coverage` -- [ ] On the build machine with docker installed, run in your OS terminal in the project dir: `make release_docker` -- [ ] Re-tag the image with `docker tag concretefhe:latest ghcr.io/zama-ai/concretefhe:vX.Y.Z` (or `vX.Y.Zrc?`) -- [ ] `docker login ghcr.io`, input your username and GitHub Personal Access Token (PAT). If not already done add `write:packages` to your PAT -- [ ] Push the release image `docker push ghcr.io/zama-ai/concretefhe:vX.Y.Z` (or `vX.Y.Zrc?`) -- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image +- [ ] Wait for the release workflow to finish and get the image url from the notification or the logs +- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link copied from the step before \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image All done! diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index bc1cbe91d..2436024d7 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -4,6 +4,8 @@ on: push: branches: - main + tags: + - "v*" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -28,7 +30,7 @@ env: ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} jobs: - build_preflight_docker: + build-preflight-docker: concurrency: group: ${{ github.ref }} cancel-in-progress: true @@ -104,14 +106,14 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} build: - needs: [build_preflight_docker] + needs: [build-preflight-docker] concurrency: group: ${{ github.ref }} cancel-in-progress: true runs-on: ubuntu-20.04 container: - image: ${{ needs.build_preflight_docker.outputs.image }} + image: ${{ needs.build-preflight-docker.outputs.image }} credentials: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} @@ -269,8 +271,8 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} push-docker-image: - needs: [build_preflight_docker, build] - if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main' && fromJSON(needs.build_preflight_docker.outputs.needs-push)) || fromJSON(needs.build_preflight_docker.outputs.force-rebuild-docker) }} + needs: [build-preflight-docker, build] + if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main' && fromJSON(needs.build-preflight-docker.outputs.needs-push)) || fromJSON(needs.build-preflight-docker.outputs.force-rebuild-docker) }} concurrency: group: ${{ github.ref }} @@ -279,7 +281,7 @@ jobs: name: Push env docker image runs-on: ubuntu-20.04 env: - PREFLIGHT_IMAGE: ${{ needs.build_preflight_docker.outputs.image }} + PREFLIGHT_IMAGE: ${{ needs.build-preflight-docker.outputs.image }} steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f @@ -310,7 +312,69 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Publishing docker image ${{ env.BASE_IMAGE }} finished with status \ + SLACK_MESSAGE: "Pushing docker image ${{ env.BASE_IMAGE }} finished with status \ + ${{ job.status }} (${{ env.ACTION_RUN_URL }})" + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + package-release: + needs: [build] + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }} + + concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + + name: Prepare docker image release + runs-on: ubuntu-20.04 + + env: + RELEASE_IMAGE_BASE: ghcr.io/zama-ai/concretefhe + + steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - name: Set tag in env + run: | + GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') + RELEASE_IMG_TAG="${RELEASE_IMAGE_BASE}:${GIT_TAG}" + echo "RELEASE_IMG_TAG=${RELEASE_IMG_TAG}" >> $GITHUB_ENV + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 + - name: Login to GitHub Container Registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ghcr.io + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_TOKEN }} + - name: Build concretefhe Image + if: ${{ success() && !cancelled() }} + uses: docker/build-push-action@a66e35b9cbcf4ad0ea91ffcaf7bbad63ad9e0229 + with: + context: . + builder: ${{ steps.buildx.outputs.name }} + file: docker/Dockerfile.release + load: true + push: false + tags: "${{ env.RELEASE_IMG_TAG }}" + no-cache: true + - name: Release image sanity check and push + if: ${{ success() && !cancelled() }} + run: | + echo "Running sanity check for ${RELEASE_IMG_TAG}" + docker run --rm -it --env LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so \ + -v "$(pwd)"/docker/release_resources:/data \ + "${RELEASE_IMG_TAG}" /bin/bash -c "python ./sanity_check.py" + docker push "${RELEASE_IMG_TAG}" + - name: Slack Notification + if: ${{ always() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Pushing docker image ${{ env.RELEASE_IMG_TAG }} finished with status \ ${{ job.status }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/docker/release_resources/sanity_check.py b/docker/release_resources/sanity_check.py new file mode 100644 index 000000000..ba04f5a7a --- /dev/null +++ b/docker/release_resources/sanity_check.py @@ -0,0 +1,36 @@ +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x + 42 + + n_bits = 3 + x = hnp.EncryptedScalar(hnp.UnsignedInteger(n_bits)) + + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** n_bits)], + ) + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** n_bits - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + result_i = engine.run(*input_i) + + if result_i == label_i: + correct += 1 + + +if __name__ == "__main__": + main() From 8571d8bfc805d1e4ca731b5c021e89f3a7da86f3 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 17:53:24 +0200 Subject: [PATCH 0292/1104] fix(build): check latest env image to decide a rebuild - we now have preflight images which break the previous logic of checking the most recent image timestamp, check image tagged with latest --- script/actions_utils/container_timestamp_check.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/actions_utils/container_timestamp_check.sh b/script/actions_utils/container_timestamp_check.sh index 902171e7f..a36181f5f 100755 --- a/script/actions_utils/container_timestamp_check.sh +++ b/script/actions_utils/container_timestamp_check.sh @@ -58,7 +58,8 @@ ENV_JSON=$(curl \ -H "Authorization: token ${TOKEN}" \ "${ENV_IMG_ENDPOINT_URL}") -ENV_IMG_TIMESTAMP=$(echo "${ENV_JSON}" | jq -r 'sort_by(.updated_at)[-1].updated_at') +ENV_IMG_TIMESTAMP=$(echo "${ENV_JSON}" | \ +jq -rc '.[] | select(.metadata.container.tags[] | contains("latest")).updated_at' echo "Base timestamp: ${BASE_IMG_TIMESTAMP}" echo "Env timestamp: ${ENV_IMG_TIMESTAMP}" From aaaaf5fd08bd4932a04f3bb6fc0ef7bf644fa480 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 09:07:11 +0200 Subject: [PATCH 0293/1104] fix(tools): fix badly written shell file --- script/actions_utils/container_timestamp_check.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/actions_utils/container_timestamp_check.sh b/script/actions_utils/container_timestamp_check.sh index a36181f5f..e6cda5bcd 100755 --- a/script/actions_utils/container_timestamp_check.sh +++ b/script/actions_utils/container_timestamp_check.sh @@ -59,7 +59,7 @@ ENV_JSON=$(curl \ "${ENV_IMG_ENDPOINT_URL}") ENV_IMG_TIMESTAMP=$(echo "${ENV_JSON}" | \ -jq -rc '.[] | select(.metadata.container.tags[] | contains("latest")).updated_at' +jq -rc '.[] | select(.metadata.container.tags[] | contains("latest")).updated_at') echo "Base timestamp: ${BASE_IMG_TIMESTAMP}" echo "Env timestamp: ${ENV_IMG_TIMESTAMP}" From 3e5ce49b0d27b5654a5fb3111d289f248aa32f3d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 20 Sep 2021 18:51:41 +0200 Subject: [PATCH 0294/1104] chore(tools): add docker to the dependencies checked by dependabot --- .github/dependabot.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 4299d7522..c2bba0ea8 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,9 +1,23 @@ version: 2 -updates: +registries: + ghcr-io: # Define access for a private registry + type: docker-registry + url: ghcr.io + username: ${{secrets.BOT_USERNAME}} + password: ${{secrets.BOT_TOKEN}} +updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every sunday interval: "weekly" day: "sunday" + + - package-ecosystem: "docker" + directory: "/docker" + registries: + - ghcr-io # Allow version updates for dependencies in this registry + schedule: + interval: "weekly" + day: "sunday" From 58ecf56c7c7c70623b933a42f1aeada2479b6e1b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 10:27:39 +0200 Subject: [PATCH 0295/1104] fix(build): fix release workflow - consider changing the method to get changed files --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 2436024d7..4ff6ccb39 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Get changed files - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} + if: ${{ (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'pull_request' }} uses: Ana06/get-changed-files@ea75ed777daf24d6e95f43957bd26b1eab20806c id: files with: From e11570e973220b3e4cd5f5ff9c565a0917c3b69e Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 21 Sep 2021 10:21:05 +0300 Subject: [PATCH 0296/1104] doc: replace usages of 'concrete' with 'Concrete' --- docs/dev/explanation/COMPILATION.md | 2 +- docs/user/advanced_examples/QuantizedLinearRegression.ipynb | 2 +- .../advanced_examples/QuantizedLogisticRegression.ipynb | 2 +- docs/user/tutorial/ARITHMETIC_OPERATIONS.md | 2 +- docs/user/tutorial/COMPILATION_ARTIFACTS.md | 2 +- docs/user/tutorial/TABLE_LOOKUP.md | 6 +++--- docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index cf6f08f8e..5e5f7e393 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -10,7 +10,7 @@ However, one can already build interesting and impressing use cases, and more wi ## How can I use it? ```python -# Import necessary concrete components +# Import necessary Concrete components import concrete.numpy as hnp # Define the function to homomorphize diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index f92c68968..61d59c9be 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -589,7 +589,7 @@ "id": "c6e101ae", "metadata": {}, "source": [ - "### Let's import the concrete numpy package now!" + "### Let's import the Concrete numpy package now!" ] }, { diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index 03db2b1f9..c28845e5b 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -684,7 +684,7 @@ "id": "34c675ed", "metadata": {}, "source": [ - "### Let's import the concrete numpy package now!" + "### Let's import the Concrete numpy package now!" ] }, { diff --git a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md index 14ab6cb94..b9ef6d145 100644 --- a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md +++ b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md @@ -1,6 +1,6 @@ # Arithmetic Operations -In this tutorial, we are going to go over all arithmetic operations available in **concrete**. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. +In this tutorial, we are going to go over all arithmetic operations available in **Concrete**. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. ## Addition diff --git a/docs/user/tutorial/COMPILATION_ARTIFACTS.md b/docs/user/tutorial/COMPILATION_ARTIFACTS.md index 576339e91..8000d63d5 100644 --- a/docs/user/tutorial/COMPILATION_ARTIFACTS.md +++ b/docs/user/tutorial/COMPILATION_ARTIFACTS.md @@ -12,7 +12,7 @@ def f(x): return np.sin(x) ``` -This function fails (for now) to compile because `concrete` doesn't support floating point outputs. When you try to compile it (you might want to check [this](../howto/COMPILING_AND_EXECUTING.md) to see how you can do that), an exception will be raised and the artifacts will be exported automatically. +This function fails (for now) to compile because `Concrete` doesn't support floating point outputs. When you try to compile it (you might want to check [this](../howto/COMPILING_AND_EXECUTING.md) to see how you can do that), an exception will be raised and the artifacts will be exported automatically. ### environment.txt diff --git a/docs/user/tutorial/TABLE_LOOKUP.md b/docs/user/tutorial/TABLE_LOOKUP.md index d2bb54d9c..600a46957 100644 --- a/docs/user/tutorial/TABLE_LOOKUP.md +++ b/docs/user/tutorial/TABLE_LOOKUP.md @@ -1,10 +1,10 @@ # Table Lookup -In this tutorial, we are going to go over the ways to perform table lookups in **concrete**. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. +In this tutorial, we are going to go over the ways to perform table lookups in **Concrete**. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile the functions below. ## Direct table lookup -**concrete** provides a special class to allow direct table lookups. Here is how to import and use it: +**Concrete** provides a special class to allow direct table lookups. Here is how to import and use it: ```python from concrete.common.extensions.table import LookupTable @@ -31,7 +31,7 @@ engine.run(3) == 0 ## Fused table lookup -Direct tables are tedious to prepare by hand. When possible, **concrete** fuses the floating point operations into a single table lookup automatically. There are some limitations on fusing operations, which you can learn more about on the next tutorial, [Working With Floating Points](./WORKING_WITH_FLOATING_POINTS.md). +Direct tables are tedious to prepare by hand. When possible, **Concrete** fuses the floating point operations into a single table lookup automatically. There are some limitations on fusing operations, which you can learn more about on the next tutorial, [Working With Floating Points](./WORKING_WITH_FLOATING_POINTS.md). Here is an example function that results in fused table lookup: diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md index 0fe234602..7fbc98905 100644 --- a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -61,7 +61,7 @@ The following operations are supported in the latest release, and we'll add more ## Limitations -Floating point support in **concrete** is very limited for the time being. They can't appear on inputs, or they can't be outputs. However, they can be used in intermediate results. Unfortunately, there are limitations on that front as well. +Floating point support in **Concrete** is very limited for the time being. They can't appear on inputs, or they can't be outputs. However, they can be used in intermediate results. Unfortunately, there are limitations on that front as well. This biggest one is that, because floating point operations are fused into table lookups with a single unsigned integer input and single unsigned integer output, only univariate portion of code can be replaced with table lookups, which means multivariate portions cannot be compiled. From 1a340e111e5e5f2cae9bf23f07ca70ed83a11850 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 10:00:34 +0200 Subject: [PATCH 0297/1104] tools: add shell_lint target to lint .sh files - update docker image to install shellcheck - fix lint errors in files --- Makefile | 8 +++++++- docker/Dockerfile.concretefhe-env | 3 ++- docker/build_release_image.sh | 2 +- script/actions_utils/container_timestamp_check.sh | 8 ++++---- script/actions_utils/coverage.sh | 6 +++--- script/make_utils/ncpus.sh | 2 +- script/make_utils/upgrade_deps.sh | 2 ++ .../benchmark_and_publish_findings_in_docker.sh | 8 ++++++-- script/source_format/format_python.sh | 6 +++--- 9 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 1202ad180..db4c2591d 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,7 @@ pcc: --no-print-directory pcc_internal .PHONY: pcc -pcc_internal: check_python_format check_finalize_nb python_linting mypy_ci pydocstyle +pcc_internal: check_python_format check_finalize_nb python_linting mypy_ci pydocstyle shell_lint .PHONY: pcc_internal pytest: @@ -203,3 +203,9 @@ upgrade_py_deps: test_codeblocks: poetry run python ./script/make_utils/test_md_python_code.py --md_dir docs/ .PHONY: test_codeblocks + +# From https://stackoverflow.com/a/63523300 for the find command +shell_lint: + find \( -path "./.venv" -o -path "./.docker_venv" \) -prune -o -type f -name "*.sh" -print | \ + xargs shellcheck +.PHONY: shell_lint diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index 78a1015d9..f2a126fe6 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -9,7 +9,8 @@ RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ python-is-python3 \ git \ graphviz* \ - pandoc && \ + pandoc \ + shellcheck && \ rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry diff --git a/docker/build_release_image.sh b/docker/build_release_image.sh index 1b0a23683..8ad9c8677 100755 --- a/docker/build_release_image.sh +++ b/docker/build_release_image.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -CURR_DIR=$(dirname $0) +CURR_DIR=$(dirname "$0") DOCKER_BUILDKIT=1 docker build --pull --no-cache -f "$CURR_DIR/Dockerfile.release" \ -t concretefhe-release "$CURR_DIR/.." diff --git a/script/actions_utils/container_timestamp_check.sh b/script/actions_utils/container_timestamp_check.sh index e6cda5bcd..7337102b0 100755 --- a/script/actions_utils/container_timestamp_check.sh +++ b/script/actions_utils/container_timestamp_check.sh @@ -38,7 +38,7 @@ do *) echo "Unknown param : $1" - exit -1 + exit 1 ;; esac shift @@ -64,8 +64,8 @@ jq -rc '.[] | select(.metadata.container.tags[] | contains("latest")).updated_at echo "Base timestamp: ${BASE_IMG_TIMESTAMP}" echo "Env timestamp: ${ENV_IMG_TIMESTAMP}" -BASE_IMG_DATE=$(date -d ${BASE_IMG_TIMESTAMP} +%s) -ENV_IMG_DATE=$(date -d ${ENV_IMG_TIMESTAMP} +%s) +BASE_IMG_DATE=$(date -d "${BASE_IMG_TIMESTAMP}" +%s) +ENV_IMG_DATE=$(date -d "${ENV_IMG_TIMESTAMP}" +%s) echo "Base epoch: ${BASE_IMG_DATE}" echo "Env epoch: ${ENV_IMG_DATE}" @@ -76,7 +76,7 @@ if [[ "${BASE_IMG_DATE}" -ge "${ENV_IMG_DATE}" ]]; then -X POST \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${TOKEN}" \ - https://api.github.com/repos/${ORG_REPO}/dispatches \ + https://api.github.com/repos/"${ORG_REPO}"/dispatches \ -d "{\"event_type\":\"${EVENT_TYPE}\"}" else echo "Image up to date, nothing to do." diff --git a/script/actions_utils/coverage.sh b/script/actions_utils/coverage.sh index 216e31d87..90f400e2f 100755 --- a/script/actions_utils/coverage.sh +++ b/script/actions_utils/coverage.sh @@ -3,13 +3,13 @@ set -o pipefail set +e -CURR_DIR=`dirname $0` +CURR_DIR=$(dirname "$0") # Run diff-coverage if [[ "$1" == "" ]]; then - BB="origin/main" + export BB="origin/main" else - BB="origin/$1" + export BB="origin/$1" fi make coverage | tee diff-coverage.txt diff --git a/script/make_utils/ncpus.sh b/script/make_utils/ncpus.sh index 2c8cfb618..61590effd 100755 --- a/script/make_utils/ncpus.sh +++ b/script/make_utils/ncpus.sh @@ -1,6 +1,6 @@ #!/bin/bash -if [[ `uname` == "Darwin" ]]; then +if [[ $(uname) == "Darwin" ]]; then sysctl -n hw.logicalcpu else nproc diff --git a/script/make_utils/upgrade_deps.sh b/script/make_utils/upgrade_deps.sh index e5b40e8bc..778cc9310 100755 --- a/script/make_utils/upgrade_deps.sh +++ b/script/make_utils/upgrade_deps.sh @@ -10,7 +10,9 @@ dev_file=$(mktemp --suffix=.txt) poetry show -o -t --no-dev | grep -v -e "--" | cut -d " " -f 1 | sed 's/$/\@latest/g' > "${no_dev_file}" poetry show -o -t | grep -v -e "--" | cut -d " " -f 1 | sed 's/$/\@latest/g' > "${all_file}" join -v1 -v2 "${all_file}" "${no_dev_file}" > "${dev_file}" +# shellcheck disable=SC2002 cat "${no_dev_file}" | xargs poetry add +# shellcheck disable=SC2002 cat "${dev_file}" | xargs poetry add --dev rm "${no_dev_file}" diff --git a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh index 115d7f858..31e245a5e 100755 --- a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh +++ b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -3,9 +3,12 @@ # Run benchmarks while logging the intermediate results # Publish findings in the progress tracker -source /src/.docker_venv/bin/activate -if [[ "$?" != "0" ]]; then +set -e + +# shellcheck disable=SC1091 +if source /src/.docker_venv/bin/activate; then python3 -m venv /src/.docker_venv + # shellcheck disable=SC1091 source /src/.docker_venv/bin/activate cd /src/ && make setup_env fi @@ -27,6 +30,7 @@ if [ -f .env ] then # Set the last two environment variables in `.env` for the curl command below # (https://gist.github.com/mihow/9c7f559807069a03e302605691f85572) + # shellcheck disable=SC2002,SC2046 export $(cat .env | tail -n 2 | sed 's/#.*//g' | xargs -d '\n') fi diff --git a/script/source_format/format_python.sh b/script/source_format/format_python.sh index 3ef6c090c..294515ef3 100755 --- a/script/source_format/format_python.sh +++ b/script/source_format/format_python.sh @@ -30,16 +30,16 @@ do *) echo "Unknown param : $1" - exit -1 + exit 1 ;; esac shift done for SRC_DIR in "${DIRS[@]}"; do - isort --profile black ${CHECK} ${SRC_DIR} + isort --profile black "${CHECK}" "${SRC_DIR}" ((FAILURES+=$?)) - black -l 100 ${CHECK} ${SRC_DIR} + black -l 100 "${CHECK}" "${SRC_DIR}" ((FAILURES+=$?)) done From 21a2656a106ae78f743e78aa00041a160a67ae36 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 11:09:26 +0200 Subject: [PATCH 0298/1104] fix(release): avoid failure with non TTY input device --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 4ff6ccb39..3bcd5111f 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -362,7 +362,7 @@ jobs: if: ${{ success() && !cancelled() }} run: | echo "Running sanity check for ${RELEASE_IMG_TAG}" - docker run --rm -it --env LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so \ + docker run --rm --env LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so \ -v "$(pwd)"/docker/release_resources:/data \ "${RELEASE_IMG_TAG}" /bin/bash -c "python ./sanity_check.py" docker push "${RELEASE_IMG_TAG}" From a0c2e67c1cd2b9eafba20ee4d3500befb17fa665 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 12:20:19 +0200 Subject: [PATCH 0299/1104] chore: bump version to v0.1.0rc3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 042650872..775ee7c4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.1.0rc2" +version = "0.1.0rc3" description = "Concrete Framework" authors = ["Zama "] packages = [ From 90fa06f80a72021576a6d9309f03ddca73a60d8d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 14:57:49 +0200 Subject: [PATCH 0300/1104] chore: less notifications - restructure the way we generate notifications --- .github/workflows/continuous-integration.yaml | 131 ++++++++++++++---- .github/workflows/daily-benchmarks.yaml | 10 +- .github/workflows/package-watcher.yaml | 15 ++ 3 files changed, 130 insertions(+), 26 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 3bcd5111f..c4336cb29 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -41,6 +41,7 @@ jobs: image: ${{ steps.set_image.outputs.image || env.LATEST_IMAGE }} needs-push: ${{ env.BUILD_DOCKER }} force-rebuild-docker: ${{ env.FORCE_REBUILD_DOCKER }} + report: ${{ steps.report.outputs.report || 'Did not run.' }} steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f @@ -53,14 +54,14 @@ jobs: - name: Should rebuild docker check run : | set +e - echo "${{ steps.files.outputs.all }}" | grep ${ENV_DOCKERFILE} + echo "${{ steps.files.outputs.all }}" | grep "${ENV_DOCKERFILE}" DOCKERFILE_CHANGED=$? if [[ "${DOCKERFILE_CHANGED}" == "0" || "${FORCE_REBUILD_DOCKER}" == "true" ]]; then echo "Should rebuild docker image!" - echo "BUILD_DOCKER=true" >> $GITHUB_ENV + echo "BUILD_DOCKER=true" >> "$GITHUB_ENV" else echo "Docker image up to date." - echo "BUILD_DOCKER=false" >> $GITHUB_ENV + echo "BUILD_DOCKER=false" >> "$GITHUB_ENV" fi - name: Set prefligh Docker image id: set_image @@ -69,7 +70,7 @@ jobs: PREFLIGHT_IMAGE_TAG=$(echo ${{ github.ref }} | sed -e 's/\//-/g') PREFLIGHT_IMAGE="${PREFLIGHT_IMAGE_BASE}-${PREFLIGHT_IMAGE_TAG}" echo "::set-output name=image::${PREFLIGHT_IMAGE}" - echo "PREFLIGHT_IMAGE=${PREFLIGHT_IMAGE}" >> $GITHUB_ENV + echo "PREFLIGHT_IMAGE=${PREFLIGHT_IMAGE}" >> "$GITHUB_ENV" - name: Set up Docker Buildx if: ${{ fromJSON(env.BUILD_DOCKER) }} id: buildx @@ -91,17 +92,24 @@ jobs: push: true tags: "${{ env.PREFLIGHT_IMAGE }}" no-cache: true - - name: Slack Notification + - name: Set notification report + id: report if: ${{ always() }} + run: | + REPORT="Docker image preflight build ${{ env.PREFLIGHT_IMAGE }} finished with \ + status ${{ job.status }}. Rebuilt image: ${{ env.BUILD_DOCKER || 'false' }}." + echo "${REPORT}" + echo "::set-output name=report::${REPORT}" + echo "REPORT=${REPORT}" >> "$GITHUB_ENV" + - name: Slack Notification + if: ${{ always() && !success() }} continue-on-error: true uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Docker image preflight build ${{ env.PREFLIGHT_IMAGE }} finished with \ - status ${{ job.status }}. Rebuilt image: ${{ env.BUILD_DOCKER || 'false' }}. \ - (${{ env.ACTION_RUN_URL }})" + SLACK_MESSAGE: "${{ env.REPORT }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -121,6 +129,9 @@ jobs: matrix: python-version: [3.8] + outputs: + report: ${{ steps.report.outputs.report || 'Did not run.' }} + steps: - name: Checkout Code uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f @@ -205,15 +216,23 @@ jobs: with: path: diff-coverage.txt recreate: true - - name: Slack Notification + - name: Set notification report + id: report if: ${{ always() }} + run: | + REPORT="Build finished with status ${{ job.status }}." + echo "${REPORT}" + echo "::set-output name=report::${REPORT}" + echo "REPORT=${REPORT}" >> "$GITHUB_ENV" + - name: Slack Notification + if: ${{ always() && !success() }} continue-on-error: true uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Build finished with status ${{ job.status }} (${{ env.ACTION_RUN_URL }})" + SLACK_MESSAGE: "${{ env.REPORT }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -223,6 +242,9 @@ jobs: group: ${{ github.ref }} cancel-in-progress: true + outputs: + report: ${{ steps.report.outputs.report || 'Did not run.' }} + runs-on: ubuntu-20.04 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} @@ -257,16 +279,24 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} DISTRIBUTION_ID: ${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }} - - name: Slack Notification + - name: Set notification report + id: report if: ${{ always() }} + run: | + REPORT="Publishing documentation finished with status ${{ job.status }}." + echo "${REPORT}" + echo "::set-output name=report::${REPORT}" + echo "REPORT=${REPORT}" >> "$GITHUB_ENV" + + - name: Slack Notification + if: ${{ always() && !success() }} continue-on-error: true uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Publishing documentation finished with status ${{ job.status }} \ - (${{ env.ACTION_RUN_URL }})" + SLACK_MESSAGE: "${{ env.REPORT }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -278,6 +308,9 @@ jobs: group: ${{ github.ref }} cancel-in-progress: true + outputs: + report: ${{ steps.report.outputs.report || 'Did not run.' }} + name: Push env docker image runs-on: ubuntu-20.04 env: @@ -293,27 +326,36 @@ jobs: password: ${{ secrets.BOT_TOKEN }} - name: Pull preflight image run: | - docker pull ${PREFLIGHT_IMAGE} + docker pull "${PREFLIGHT_IMAGE}" - name: Retag to latest and epoch-sha1 and push run: | EPOCH=$(date +%s) SHA1=$(git rev-parse HEAD) TAGGED_IMAGE="${BASE_IMAGE}:${EPOCH}-${SHA1}" - docker tag ${PREFLIGHT_IMAGE} ${LATEST_IMAGE} - docker tag ${PREFLIGHT_IMAGE} ${TAGGED_IMAGE} - docker push ${LATEST_IMAGE} - docker push ${TAGGED_IMAGE} + docker tag "${PREFLIGHT_IMAGE}" "${LATEST_IMAGE}" + docker tag "${PREFLIGHT_IMAGE}" "${TAGGED_IMAGE}" + docker push "${LATEST_IMAGE}" + docker push "${TAGGED_IMAGE}" + + - name: Set notification report + id: report + if: ${{ always() }} + run: | + REPORT="Pushing docker image ${{ env.BASE_IMAGE }} finished with status \ + ${{ job.status }}." + echo "${REPORT}" + echo "::set-output name=report::${REPORT}" + echo "REPORT=${REPORT}" >> "$GITHUB_ENV" - name: Slack Notification - if: ${{ always() }} + if: ${{ always() && !success() }} continue-on-error: true uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Pushing docker image ${{ env.BASE_IMAGE }} finished with status \ - ${{ job.status }} (${{ env.ACTION_RUN_URL }})" + SLACK_MESSAGE: "${{ env.REPORT }} (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} @@ -325,6 +367,9 @@ jobs: group: ${{ github.ref }} cancel-in-progress: true + outputs: + report: ${{ steps.report.outputs.report || 'Did not run.' }} + name: Prepare docker image release runs-on: ubuntu-20.04 @@ -337,7 +382,7 @@ jobs: run: | GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') RELEASE_IMG_TAG="${RELEASE_IMAGE_BASE}:${GIT_TAG}" - echo "RELEASE_IMG_TAG=${RELEASE_IMG_TAG}" >> $GITHUB_ENV + echo "RELEASE_IMG_TAG=${RELEASE_IMG_TAG}" >> "$GITHUB_ENV" - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 @@ -366,6 +411,40 @@ jobs: -v "$(pwd)"/docker/release_resources:/data \ "${RELEASE_IMG_TAG}" /bin/bash -c "python ./sanity_check.py" docker push "${RELEASE_IMG_TAG}" + - name: Set notification report + id: report + if: ${{ always() }} + run: | + REPORT="Pushing docker image ${{ env.RELEASE_IMG_TAG }} finished with status \ + ${{ job.status }}." + echo "${REPORT}" + echo "::set-output name=report::${REPORT}" + echo "REPORT=${REPORT}" >> "$GITHUB_ENV" + - name: Slack Notification + if: ${{ always() && !success() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "${{ env.REPORT }} (${{ env.ACTION_RUN_URL }})" + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + send-report: + if: ${{ always() }} + needs: [ + build-preflight-docker, + build, + publish-docs, + push-docker-image, + package-release, + ] + + name: Send Slack notification + runs-on: ubuntu-20.04 + steps: - name: Slack Notification if: ${{ always() }} continue-on-error: true @@ -374,7 +453,11 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: "Pushing docker image ${{ env.RELEASE_IMG_TAG }} finished with status \ - ${{ job.status }} (${{ env.ACTION_RUN_URL }})" + SLACK_MESSAGE: "Full run here: ${{ env.ACTION_RUN_URL }}\n\ + - build-preflight-docker: ${{ needs.build-preflight-docker.outputs.report || 'Did not run.' }}\n\n\ + - build: ${{ needs.build.outputs.report || 'Did not run.' }}\n\n\ + - publish-docs: ${{ needs.publish-docs.outputs.report || 'Did not run.' }}\n\n\ + - push-docker-image: ${{ needs.push-docker-image.outputs.report || 'Did not run.' }}\n\n\ + - package-release: ${{ needs.package-release.outputs.report || 'Did not run.' }}" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/daily-benchmarks.yaml b/.github/workflows/daily-benchmarks.yaml index 6101fd637..c2295f56a 100644 --- a/.github/workflows/daily-benchmarks.yaml +++ b/.github/workflows/daily-benchmarks.yaml @@ -4,6 +4,9 @@ on: schedule: - cron: '0 22 * * *' # Everyday @ 22:00 +env: + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + jobs: perform: name: Run Benchmarks on EC2 and Publish Results to Progress Tracker @@ -21,7 +24,9 @@ jobs: aws ec2 start-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} - name: Wait For The Instance To Get An IP Address - run: timeout 180 bash -c 'until [[ $(aws ec2 describe-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} --query 'Reservations[].Instances[].PublicIpAddress' --output text) != "" ]]; do sleep 0.1; done' + run: | + # shellcheck disable=SC2016,2026 + timeout 180 bash -c 'until [[ $(aws ec2 describe-instances --instance-ids ${{ secrets.BENCHMARKS_EC2_INSTANCE_ID }} --query 'Reservations[].Instances[].PublicIpAddress' --output text) != "" ]]; do sleep 0.1; done' - name: Get Public IP Address of EC2 Instance id: public-ip @@ -73,6 +78,7 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ job.status }} - SLACK_MESSAGE: 'Publishing benchmarks finished with status ${{ job.status }} (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})' + SLACK_MESSAGE: "Publishing benchmarks finished with status ${{ job.status }} \ + (${{ env.ACTION_RUN_URL }})" SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index f01f7d4c7..ef236b758 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -7,6 +7,9 @@ on: # Timezone is UTC, so Paris time is +2 during the summer and +1 during winter - cron: '0 6-20 * * 1-5' +env: + ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + jobs: check_and_notify_build: name: Check timestamps and notify build @@ -24,3 +27,15 @@ jobs: --token ${{ secrets.BOT_TOKEN }} \ --org-repo ${{ github.repository }} \ --event-type rebuild-env-docker + - name: Send Slack Notification + if: ${{ always() && failure() }} + continue-on-error: true + uses: rtCamp/action-slack-notify@12e36fc18b0689399306c2e0b3e0f2978b7f1ee7 + env: + SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} + SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png + SLACK_COLOR: ${{ job.status }} + SLACK_MESSAGE: "Package watcher finished with status ${{ job.status }} \ + (${{ env.ACTION_RUN_URL }})" + SLACK_USERNAME: ${{ secrets.BOT_USERNAME }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} From d6986a0d93c85a61cb3796057bcc36edeb7d8721 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 17:13:30 +0200 Subject: [PATCH 0301/1104] chore: make sure global notification color makes sense --- .github/workflows/continuous-integration.yaml | 31 ++++++++++---- .../actions_utils/actions_combine_status.py | 42 +++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 script/actions_utils/actions_combine_status.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index c4336cb29..1e49e1019 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -52,7 +52,7 @@ jobs: with: format: 'space-delimited' - name: Should rebuild docker check - run : | + run: | set +e echo "${{ steps.files.outputs.all }}" | grep "${ENV_DOCKERFILE}" DOCKERFILE_CHANGED=$? @@ -434,17 +434,30 @@ jobs: send-report: if: ${{ always() }} - needs: [ - build-preflight-docker, - build, - publish-docs, - push-docker-image, - package-release, - ] + needs: + [ + build-preflight-docker, + build, + publish-docs, + push-docker-image, + package-release, + ] name: Send Slack notification runs-on: ubuntu-20.04 steps: + - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - name: Prepare whole job status + if: ${{ always() }} + continue-on-error: true + env: + NEEDS_JSON: ${{ toJSON(needs) }} + run: | + echo "${NEEDS_JSON}" > /tmp/needs_context.json + JOB_STATUS=$(python3 ./script/actions_utils/actions_combine_status.py \ + --needs_context_json /tmp/needs_context.json) + echo "JOB_STATUS=${JOB_STATUS}" >> "$GITHUB_ENV" + - name: Slack Notification if: ${{ always() }} continue-on-error: true @@ -452,7 +465,7 @@ jobs: env: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png - SLACK_COLOR: ${{ job.status }} + SLACK_COLOR: ${{ env.JOB_STATUS || 'failure' }} SLACK_MESSAGE: "Full run here: ${{ env.ACTION_RUN_URL }}\n\ - build-preflight-docker: ${{ needs.build-preflight-docker.outputs.report || 'Did not run.' }}\n\n\ - build: ${{ needs.build.outputs.report || 'Did not run.' }}\n\n\ diff --git a/script/actions_utils/actions_combine_status.py b/script/actions_utils/actions_combine_status.py new file mode 100644 index 000000000..abcb41c95 --- /dev/null +++ b/script/actions_utils/actions_combine_status.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +"""Helper script for github actions to combine job statuses""" +import argparse +import json + +RESULTS_TO_DISPLAY_LEVEL = { + "failure": 0, + "cancelled": 1, + "success": 2, + "skipped": 3, +} + +DISPLAY_LEVEL_TO_RESULTS = {val: key for key, val in RESULTS_TO_DISPLAY_LEVEL.items()} + + +def main(args): + """Entry point""" + + need_context_data = None + with open(args.needs_context_json, encoding="utf-8") as f: + need_context_data = json.load(f) + + display_level = min( + RESULTS_TO_DISPLAY_LEVEL[job_object["result"]] for job_object in need_context_data.values() + ) + + print(DISPLAY_LEVEL_TO_RESULTS[display_level]) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Combine github actions statuses", allow_abbrev=False) + + parser.add_argument( + "--needs_context_json", + type=str, + help="Pass the json file path containing the workflow needs context", + ) + + cli_args = parser.parse_args() + + main(cli_args) From 468f8aba625a9cd1dcd2a7cbcf1e2ed632c3e666 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 22 Sep 2021 09:37:50 +0200 Subject: [PATCH 0302/1104] fix: correct a mistake done when fixing shellcheck lints - we must not exit right away if sourcing fails - recreate the venv only if the source fails --- .../benchmark_and_publish_findings_in_docker.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh index 31e245a5e..52b73f02c 100755 --- a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh +++ b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -3,10 +3,10 @@ # Run benchmarks while logging the intermediate results # Publish findings in the progress tracker -set -e +set +e # shellcheck disable=SC1091 -if source /src/.docker_venv/bin/activate; then +if ! source /src/.docker_venv/bin/activate; then python3 -m venv /src/.docker_venv # shellcheck disable=SC1091 source /src/.docker_venv/bin/activate From fd57055b46d7a874b2f2ba4ff929f4684a06524f Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 21 Sep 2021 12:21:46 +0200 Subject: [PATCH 0303/1104] chore: update the release template --- .github/ISSUE_TEMPLATE/release.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 9224e76c4..7ddce8251 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -2,15 +2,31 @@ name: Release about: Issue template to prepare a release step by step. title: "Release vX.Y.Z (or vX.Y.Zrc?)" +labels: "release" --- +Please check all steps if it was either done/already done, at the end of a release all check boxes must have been checked. + Release check-list: +If it was not already done: - [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Zrc?` for Release Candidates) following semantic versioning: https://semver.org/ - [ ] Update the version in pyproject.toml to `X.Y.Z` (or `X.Y.Zrc?`) -- [ ] Check the release milestone issues, cut out what can't be completed in time + +Then: +- [ ] For non RC releases: check the release milestone issues, cut out what can't be completed in time and change the milestones for these issues - [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Zrc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Zrc?`) - [ ] Wait for the release workflow to finish and get the image url from the notification or the logs -- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link copied from the step before \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image +- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link copied from the step before \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image. Mark release as pre-release for an `rc` version. See template below: + +This is the release markdown template you should copy and update: +``` +**Docker Image:** ghcr.io/zama-ai/concretefhe:vX.Y.Z +**Documentation:** https://docs.zama.ai/concrete +``` + +To continue the release cycle: +- [ ] Choose the version number for next release, e.g. `vA.B.C` (can be `vA.B.Crc?` for Release Candidates) following semantic versioning: https://semver.org/ +- [ ] Update the version in pyproject.toml to `A.B.C` (or `A.B.Crc?`) All done! From 2c19cd31228f5ed398c3ca9e5b0e3cdfc5224769 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 22 Sep 2021 13:42:18 +0200 Subject: [PATCH 0304/1104] fix(tools): another fix for format_python.sh after shellcheck changes --- script/source_format/format_python.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/source_format/format_python.sh b/script/source_format/format_python.sh index 294515ef3..d136b935c 100755 --- a/script/source_format/format_python.sh +++ b/script/source_format/format_python.sh @@ -37,9 +37,9 @@ do done for SRC_DIR in "${DIRS[@]}"; do - isort --profile black "${CHECK}" "${SRC_DIR}" + isort --profile black ${CHECK:+"$CHECK"} "${SRC_DIR}" ((FAILURES+=$?)) - black -l 100 "${CHECK}" "${SRC_DIR}" + black -l 100 ${CHECK:+"$CHECK"} "${SRC_DIR}" ((FAILURES+=$?)) done From 5e8cacd9ead41e935cc37116fdfb7d339526af69 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 22 Sep 2021 14:03:47 +0200 Subject: [PATCH 0305/1104] build: print the global job status in notification - easier to know if one should check the full notification in slack --- .github/workflows/continuous-integration.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 1e49e1019..ee2253e26 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -466,7 +466,8 @@ jobs: SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }} SLACK_ICON: https://pbs.twimg.com/profile_images/1274014582265298945/OjBKP9kn_400x400.png SLACK_COLOR: ${{ env.JOB_STATUS || 'failure' }} - SLACK_MESSAGE: "Full run here: ${{ env.ACTION_RUN_URL }}\n\ + SLACK_MESSAGE: "Full run finished with status ${{ env.JOB_STATUS || 'failure' }} \ + (${{ env.ACTION_RUN_URL }})\n\ - build-preflight-docker: ${{ needs.build-preflight-docker.outputs.report || 'Did not run.' }}\n\n\ - build: ${{ needs.build.outputs.report || 'Did not run.' }}\n\n\ - publish-docs: ${{ needs.publish-docs.outputs.report || 'Did not run.' }}\n\n\ From 98bf595324169387e4da66ac113615c3b4ebcf2c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 22 Sep 2021 12:46:51 +0200 Subject: [PATCH 0306/1104] chore(build): get last completed run to check whether to rebuild docker --- .github/workflows/continuous-integration.yaml | 27 +++++++- script/actions_utils/get_latest_run.sh | 61 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100755 script/actions_utils/get_latest_run.sh diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index ee2253e26..dc84d3074 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -45,12 +45,33 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + fetch-depth: 0 - name: Get changed files if: ${{ (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'pull_request' }} - uses: Ana06/get-changed-files@ea75ed777daf24d6e95f43957bd26b1eab20806c id: files - with: - format: 'space-delimited' + env: + IS_PR: ${{ github.event_name == 'pull_request' }} + run: | + CURRENT_SHA="${{ github.sha }}" + if [[ "${IS_PR}" == "true" ]]; then + BEFORE_SHA="${{ github.event.pull_request.base.sha }}" + else + BEFORE_SHA="$(./script/actions_utils/get_latest_run.sh \ + --token ${{ secrets.BOT_TOKEN }} \ + --org-repo ${{ github.repository }} \ + --event-types 'push workflow_dispatch repository_dispatch')" + fi + ALL_CHANGED_FILES=$(git diff --name-only "${CURRENT_SHA}" "${BEFORE_SHA}") + + echo "Before sha1 ${BEFORE_SHA}" + echo "Current sha1 ${CURRENT_SHA}" + + echo "Changed files:" + echo "${ALL_CHANGED_FILES}" + + echo "::set-output name=all::${ALL_CHANGED_FILES//$'\n'/ }" + - name: Should rebuild docker check run: | set +e diff --git a/script/actions_utils/get_latest_run.sh b/script/actions_utils/get_latest_run.sh new file mode 100755 index 000000000..add01cd14 --- /dev/null +++ b/script/actions_utils/get_latest_run.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +TOKEN= +ORG_REPO= +EVENTS_TO_CHECK= + +while [ -n "$1" ] +do + case "$1" in + "--token" ) + shift + TOKEN="$1" + ;; + + "--org-repo" ) + shift + ORG_REPO="$1" + ;; + + "--event-types" ) + shift + EVENTS_TO_CHECK="$1" + ;; + + *) + echo "Unknown param : $1" + exit 1 + ;; + esac + shift +done + +# Store the workflows that come in jsons in a file per event type +declare -a JSON_FILES_ARRAY=() +for EVENT in $EVENTS_TO_CHECK; do + CURR_FILE="$(mktemp --suffix=.json)" + curl \ + -X GET \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token ${TOKEN}" \ + "https://api.github.com/repos/${ORG_REPO}/actions/runs?branch=main&event=${EVENT}&status=success" | \ + jq -rc '.workflow_runs | sort_by(.updated_at)[-1]' > "${CURR_FILE}" + JSON_FILES_ARRAY+=("${CURR_FILE}") +done + +# Put all the workflows in the same json and dump that +CONCAT_FILE="$(mktemp --suffix=.json)" +jq -sr '.' "${JSON_FILES_ARRAY[@]}" > "${CONCAT_FILE}" + +# Sort by updated_at, get the last and get the sha1 for this last one +BEFORE_SHA=$(jq -rc 'sort_by(.updated_at)[-1].head_sha' "${CONCAT_FILE}") + +# Remove files +rm "${CONCAT_FILE}" + +for FILE_TO_RM in "${JSON_FILES_ARRAY[@]}"; do + rm "${FILE_TO_RM}" +done + +# Echo for the outside world +echo "${BEFORE_SHA}" From 9dcaf8d299dc42e6d57c1f24a17f7070b7fa5f36 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 22 Sep 2021 18:00:24 +0200 Subject: [PATCH 0307/1104] fix: remove auto label after release label deletion --- .github/ISSUE_TEMPLATE/release.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 7ddce8251..aeaa99580 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -2,7 +2,6 @@ name: Release about: Issue template to prepare a release step by step. title: "Release vX.Y.Z (or vX.Y.Zrc?)" -labels: "release" --- Please check all steps if it was either done/already done, at the end of a release all check boxes must have been checked. From 9a4ae6e443b8dc6c4bf0ae913acb3ff43eef5b87 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 23 Sep 2021 09:53:48 +0200 Subject: [PATCH 0308/1104] chore: update release base image to latest working compiler image --- docker/Dockerfile.release | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index dfe518670..d22952d06 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -1,4 +1,4 @@ -FROM ghcr.io/zama-ai/zamalang-compiler:967fda07a05b6a410fee2027514a7114bdf781e9 as builder +FROM ghcr.io/zama-ai/zamalang-compiler:3a254bcb8725507a11538913d3d5e9657ac00043 as builder RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ @@ -14,7 +14,7 @@ COPY pyproject.toml ./pyproject.toml RUN poetry build --format wheel -FROM ghcr.io/zama-ai/zamalang-compiler:967fda07a05b6a410fee2027514a7114bdf781e9 +FROM ghcr.io/zama-ai/zamalang-compiler:3a254bcb8725507a11538913d3d5e9657ac00043 RUN mkdir /pkg && mkdir /app WORKDIR /pkg From 86052fa43d5de82fe9bc7ce01cd6b79777f3a026 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 23 Sep 2021 10:50:35 +0200 Subject: [PATCH 0309/1104] chore(tools): update command line for isort to follow the 100 line length --- concrete/common/mlir/mlir_converter.py | 9 +-------- concrete/numpy/__init__.py | 9 +-------- script/source_format/format_python.sh | 2 +- tests/common/bounds_measurement/test_inputset_eval.py | 4 +--- tests/common/debugging/test_custom_assert.py | 6 +----- tests/common/representation/test_intermediate.py | 7 +------ tests/numpy/test_compile.py | 5 +---- tests/numpy/test_tracing.py | 7 +------ 8 files changed, 8 insertions(+), 41 deletions(-) diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index 2aa328ac3..7273268a3 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -5,14 +5,7 @@ from typing import Tuple, cast import networkx as nx import zamalang from mlir.dialects import builtin -from mlir.ir import ( - Context, - InsertionPoint, - IntegerType, - Location, - Module, - RankedTensorType, -) +from mlir.ir import Context, InsertionPoint, IntegerType, Location, Module, RankedTensorType from mlir.ir import Type as MLIRType from mlir.ir import UnrankedTensorType from zamalang.dialects import hlfhe diff --git a/concrete/numpy/__init__.py b/concrete/numpy/__init__.py index 97cfc0789..7e793806f 100644 --- a/concrete/numpy/__init__.py +++ b/concrete/numpy/__init__.py @@ -1,14 +1,7 @@ """Module for compiling numpy functions to homomorphic equivalents.""" from ..common.compilation import CompilationArtifacts, CompilationConfiguration -from ..common.data_types import ( - Float, - Float32, - Float64, - Integer, - SignedInteger, - UnsignedInteger, -) +from ..common.data_types import Float, Float32, Float64, Integer, SignedInteger, UnsignedInteger from ..common.debugging import draw_graph, get_printable_graph from ..common.extensions.table import LookupTable from ..common.values import ( diff --git a/script/source_format/format_python.sh b/script/source_format/format_python.sh index d136b935c..73be71883 100755 --- a/script/source_format/format_python.sh +++ b/script/source_format/format_python.sh @@ -37,7 +37,7 @@ do done for SRC_DIR in "${DIRS[@]}"; do - isort --profile black ${CHECK:+"$CHECK"} "${SRC_DIR}" + isort -l 100 --profile black ${CHECK:+"$CHECK"} "${SRC_DIR}" ((FAILURES+=$?)) black -l 100 ${CHECK:+"$CHECK"} "${SRC_DIR}" ((FAILURES+=$?)) diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index d2b07f30a..3569c8bc5 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -4,9 +4,7 @@ from typing import Tuple import pytest -from concrete.common.bounds_measurement.inputset_eval import ( - eval_op_graph_bounds_on_inputset, -) +from concrete.common.bounds_measurement.inputset_eval import eval_op_graph_bounds_on_inputset from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer from concrete.common.values import EncryptedScalar diff --git a/tests/common/debugging/test_custom_assert.py b/tests/common/debugging/test_custom_assert.py index aa34b7a85..b779f4bce 100644 --- a/tests/common/debugging/test_custom_assert.py +++ b/tests/common/debugging/test_custom_assert.py @@ -1,11 +1,7 @@ """Test custom assert functions.""" import pytest -from concrete.common.debugging.custom_assert import ( - assert_false, - assert_not_reached, - assert_true, -) +from concrete.common.debugging.custom_assert import assert_false, assert_not_reached, assert_true def test_assert_not_functions(): diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index b99668bac..34251437e 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -6,12 +6,7 @@ import pytest from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer from concrete.common.representation import intermediate as ir -from concrete.common.values import ( - ClearScalar, - ClearTensor, - EncryptedScalar, - EncryptedTensor, -) +from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor @pytest.mark.parametrize( diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index be4e47432..3aacdb737 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -10,10 +10,7 @@ from concrete.common.data_types.integers import Integer from concrete.common.debugging import draw_graph, get_printable_graph from concrete.common.extensions.table import LookupTable from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor -from concrete.numpy.compile import ( - compile_numpy_function, - compile_numpy_function_into_op_graph, -) +from concrete.numpy.compile import compile_numpy_function, compile_numpy_function_into_op_graph def no_fuse_unhandled(x, y): diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 1df0e01d0..76634a779 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -7,12 +7,7 @@ import pytest from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer from concrete.common.representation import intermediate as ir -from concrete.common.values import ( - ClearScalar, - ClearTensor, - EncryptedScalar, - EncryptedTensor, -) +from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor from concrete.numpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] From b971f6b913833372acb95e97bd99667a03dad01d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 23 Sep 2021 10:50:05 +0200 Subject: [PATCH 0310/1104] refactor: remove as ir imports to avoid breaking sphinx docs links --- concrete/common/common_helpers.py | 10 ++-- concrete/common/compilation/artifacts.py | 8 +-- concrete/common/debugging/drawing.py | 28 +++++++---- concrete/common/debugging/printing.py | 8 +-- concrete/common/extensions/table.py | 4 +- concrete/common/mlir/converters.py | 14 +++--- concrete/common/mlir/mlir_converter.py | 4 +- concrete/common/operator_graph.py | 52 +++++++++---------- concrete/common/optimization/topological.py | 56 ++++++++++----------- concrete/common/tracing/base_tracer.py | 25 +++++---- concrete/common/tracing/tracing_helpers.py | 4 +- concrete/numpy/compile.py | 4 +- 12 files changed, 115 insertions(+), 102 deletions(-) diff --git a/concrete/common/common_helpers.py b/concrete/common/common_helpers.py index 53b0989f8..7aa55eda4 100644 --- a/concrete/common/common_helpers.py +++ b/concrete/common/common_helpers.py @@ -5,7 +5,7 @@ from typing import List, Optional from .data_types.integers import Integer from .debugging import custom_assert from .operator_graph import OPGraph -from .representation import intermediate as ir +from .representation.intermediate import IntermediateNode def is_a_power_of_2(x: int) -> bool: @@ -22,11 +22,11 @@ def is_a_power_of_2(x: int) -> bool: return x > 0 and (x & (x - 1)) == 0 -def ir_nodes_has_integer_input_and_output(node: ir.IntermediateNode) -> bool: +def ir_nodes_has_integer_input_and_output(node: IntermediateNode) -> bool: """Check if an ir node has Integer inputs and outputs. Args: - node (ir.IntermediateNode): Node to check + node (IntermediateNode): Node to check Returns: bool: True if all input and output values hold Integers @@ -40,13 +40,13 @@ def ir_nodes_has_integer_input_and_output(node: ir.IntermediateNode) -> bool: # long run probably def check_op_graph_is_integer_program( op_graph: OPGraph, - offending_nodes_out: Optional[List[ir.IntermediateNode]] = None, + offending_nodes_out: Optional[List[IntermediateNode]] = None, ) -> bool: """Check if an op_graph inputs, outputs and intermediate values are Integers. Args: op_graph (OPGraph): The OPGraph to check - offending_nodes_out (Optional[List[ir.IntermediateNode]]): Optionally pass a list that will + offending_nodes_out (Optional[List[IntermediateNode]]): Optionally pass a list that will be populated with offending nodes, the list will be cleared before being filled Returns: diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index c1af6be45..88746a10d 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -12,7 +12,7 @@ from PIL import Image from ..debugging import custom_assert, draw_graph, get_printable_graph from ..operator_graph import OPGraph -from ..representation import intermediate as ir +from ..representation.intermediate import IntermediateNode from ..values import BaseValue DEFAULT_OUTPUT_DIRECTORY: Path = Path(".artifacts") @@ -30,7 +30,7 @@ class CompilationArtifacts: textual_representations_of_operation_graphs: Dict[str, str] final_operation_graph: Optional[OPGraph] - bounds_of_the_final_operation_graph: Optional[Dict[ir.IntermediateNode, Dict[str, Any]]] + bounds_of_the_final_operation_graph: Optional[Dict[IntermediateNode, Dict[str, Any]]] mlir_of_the_final_operation_graph: Optional[str] def __init__(self, output_directory: Path = DEFAULT_OUTPUT_DIRECTORY): @@ -92,11 +92,11 @@ class CompilationArtifacts: self.final_operation_graph = operation_graph - def add_final_operation_graph_bounds(self, bounds: Dict[ir.IntermediateNode, Dict[str, Any]]): + def add_final_operation_graph_bounds(self, bounds: Dict[IntermediateNode, Dict[str, Any]]): """Add the bounds of the final operation graph to the artifacts. Args: - bounds (Dict[ir.IntermediateNode, Dict[str, Any]]): the bound dictionary + bounds (Dict[IntermediateNode, Dict[str, Any]]): the bound dictionary Returns: None diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index cb088c24b..c06115996 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -11,17 +11,25 @@ from PIL import Image from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph -from ..representation import intermediate as ir -from ..representation.intermediate import ALL_IR_NODES +from ..representation.intermediate import ( + ALL_IR_NODES, + Add, + ArbitraryFunction, + Constant, + Dot, + Input, + Mul, + Sub, +) IR_NODE_COLOR_MAPPING = { - ir.Input: "blue", - ir.Constant: "cyan", - ir.Add: "red", - ir.Sub: "yellow", - ir.Mul: "green", - ir.ArbitraryFunction: "orange", - ir.Dot: "purple", + Input: "blue", + Constant: "cyan", + Add: "red", + Sub: "yellow", + Mul: "green", + ArbitraryFunction: "orange", + Dot: "purple", "ArbitraryFunction": "orange", "TLU": "grey", "output": "magenta", @@ -63,7 +71,7 @@ def draw_graph( value_to_return = IR_NODE_COLOR_MAPPING[type(node)] if node in output_nodes: value_to_return = IR_NODE_COLOR_MAPPING["output"] - elif isinstance(node, ir.ArbitraryFunction): + elif isinstance(node, ArbitraryFunction): value_to_return = IR_NODE_COLOR_MAPPING.get(node.op_name, value_to_return) return value_to_return diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 3b3123f82..f82fb8203 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -6,7 +6,7 @@ import networkx as nx from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph -from ..representation import intermediate as ir +from ..representation.intermediate import ArbitraryFunction, Constant, Input def output_data_type_to_string(node): @@ -49,15 +49,15 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: # they only are done by incrementing i custom_assert(len(node.outputs) == 1) - if isinstance(node, ir.Input): + if isinstance(node, Input): what_to_print = node.input_name - elif isinstance(node, ir.Constant): + elif isinstance(node, Constant): what_to_print = f"Constant({node.constant_data})" else: base_name = node.__class__.__name__ - if isinstance(node, ir.ArbitraryFunction): + if isinstance(node, ArbitraryFunction): base_name = node.op_name what_to_print = base_name + "(" diff --git a/concrete/common/extensions/table.py b/concrete/common/extensions/table.py index 8fc3eac87..971a4309f 100644 --- a/concrete/common/extensions/table.py +++ b/concrete/common/extensions/table.py @@ -6,7 +6,7 @@ from typing import Iterable, Tuple, Union from ..common_helpers import is_a_power_of_2 from ..data_types.base import BaseDataType from ..data_types.integers import make_integer_to_hold -from ..representation import intermediate as ir +from ..representation.intermediate import ArbitraryFunction from ..tracing.base_tracer import BaseTracer @@ -35,7 +35,7 @@ class LookupTable: # we need to create an `ArbitraryFunction` node # because the result will be determined during the runtime if isinstance(key, BaseTracer): - traced_computation = ir.ArbitraryFunction( + traced_computation = ArbitraryFunction( input_base_value=key.output, arbitrary_func=LookupTable._checked_indexing, output_dtype=self.output_dtype, diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 746013839..b9bc1452a 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -22,7 +22,7 @@ from ..data_types.dtypes_helpers import ( value_is_encrypted_tensor_integer, ) from ..debugging.custom_assert import custom_assert -from ..representation import intermediate as ir +from ..representation.intermediate import Add, ArbitraryFunction, Constant, Dot, Mul, Sub def add(node, preds, ir_to_mlir_node, ctx): @@ -189,12 +189,12 @@ def dot(node, preds, ir_to_mlir_node, ctx): V0_OPSET_CONVERSION_FUNCTIONS = { - ir.Add: add, - ir.Sub: sub, - ir.Mul: mul, - ir.Constant: constant, - ir.ArbitraryFunction: apply_lut, - ir.Dot: dot, + Add: add, + Sub: sub, + Mul: mul, + Constant: constant, + ArbitraryFunction: apply_lut, + Dot: dot, } # pylint: enable=no-name-in-module,no-member diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index 7273268a3..aec6b7906 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -20,7 +20,7 @@ from ..data_types.dtypes_helpers import ( ) from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph -from ..representation import intermediate as ir +from ..representation.intermediate import Input class MLIRConverter: @@ -151,7 +151,7 @@ class MLIRConverter: for arg_num, node in op_graph.input_nodes.items(): ir_to_mlir_node[node] = arg[arg_num] for node in nx.topological_sort(op_graph.graph): - if isinstance(node, ir.Input): + if isinstance(node, Input): continue mlir_op = self.conversion_functions.get(type(node), None) if mlir_op is None: # pragma: no cover diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index 1c5ee104e..7d8464cb9 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -13,7 +13,7 @@ from .data_types.dtypes_helpers import ( from .data_types.floats import Float from .data_types.integers import Integer, make_integer_to_hold from .debugging.custom_assert import custom_assert -from .representation import intermediate as ir +from .representation.intermediate import Input, IntermediateNode from .tracing import BaseTracer from .tracing.tracing_helpers import create_graph_from_output_tracers @@ -22,25 +22,25 @@ class OPGraph: """Class to make work with nx graphs easier.""" graph: nx.MultiDiGraph - input_nodes: Dict[int, ir.Input] - output_nodes: Dict[int, ir.IntermediateNode] + input_nodes: Dict[int, Input] + output_nodes: Dict[int, IntermediateNode] def __init__( self, graph: nx.MultiDiGraph, - input_nodes: Dict[int, ir.Input], - output_nodes: Dict[int, ir.IntermediateNode], + input_nodes: Dict[int, Input], + output_nodes: Dict[int, IntermediateNode], ) -> None: custom_assert( len(input_nodes) > 0, "Got a graph without input nodes which is not supported" ) custom_assert( - all(isinstance(node, ir.Input) for node in input_nodes.values()), - "Got input nodes that were not ir.Input, which is not supported", + all(isinstance(node, Input) for node in input_nodes.values()), + "Got input nodes that were not Input, which is not supported", ) custom_assert( - all(isinstance(node, ir.IntermediateNode) for node in output_nodes.values()), - "Got output nodes which were not ir.IntermediateNode, which is not supported", + all(isinstance(node, IntermediateNode) for node in output_nodes.values()), + "Got output nodes which were not IntermediateNode, which is not supported", ) self.graph = graph @@ -75,7 +75,7 @@ class OPGraph: input_nodes = { node.program_input_idx: node for node in graph.nodes() - if len(graph.pred[node]) == 0 and isinstance(node, ir.Input) + if len(graph.pred[node]) == 0 and isinstance(node, Input) } output_nodes = { output_idx: tracer.traced_computation @@ -86,50 +86,50 @@ class OPGraph: @staticmethod def from_graph( graph: nx.MultiDiGraph, - input_nodes: Iterable[ir.Input], - output_nodes: Iterable[ir.IntermediateNode], + input_nodes: Iterable[Input], + output_nodes: Iterable[IntermediateNode], ) -> "OPGraph": """Construct OPGraph from an existing networkx MultiDiGraph. Args: graph (nx.MultiDiGraph): The networkx MultiDiGraph to use. - input_nodes (Iterable[ir.Input]): The input nodes of the MultiDiGraph. - output_nodes (Iterable[ir.IntermediateNode]): The output nodes of the MultiDiGraph. + input_nodes (Iterable[Input]): The input nodes of the MultiDiGraph. + output_nodes (Iterable[IntermediateNode]): The output nodes of the MultiDiGraph. Returns: OPGraph: The resulting OPGraph. """ return OPGraph(graph, dict(enumerate(input_nodes)), dict(enumerate(output_nodes))) - def get_ordered_inputs(self) -> List[ir.Input]: + def get_ordered_inputs(self) -> List[Input]: """Get the input nodes of the graph, ordered by their index. Returns: - List[ir.Input]: ordered input nodes + List[Input]: ordered input nodes """ return [self.input_nodes[idx] for idx in range(len(self.input_nodes))] - def get_ordered_outputs(self) -> List[ir.IntermediateNode]: + def get_ordered_outputs(self) -> List[IntermediateNode]: """Get the output nodes of the graph, ordered by their index. Returns: - List[ir.IntermediateNode]: ordered input nodes + List[IntermediateNode]: ordered input nodes """ return [self.output_nodes[idx] for idx in range(len(self.output_nodes))] - def evaluate(self, inputs: Dict[int, Any]) -> Dict[ir.IntermediateNode, Any]: + def evaluate(self, inputs: Dict[int, Any]) -> Dict[IntermediateNode, Any]: """Evaluate a graph and get intermediate values for all nodes. Args: inputs (Dict[int, Any]): The inputs to the program Returns: - Dict[ir.IntermediateNode, Any]: Dictionary with node as keys and resulting values + Dict[IntermediateNode, Any]: Dictionary with node as keys and resulting values """ - node_results: Dict[ir.IntermediateNode, Any] = {} + node_results: Dict[IntermediateNode, Any] = {} for node in nx.topological_sort(self.graph): - if not isinstance(node, ir.Input): + if not isinstance(node, Input): curr_inputs = {} for pred_node in self.graph.pred[node]: edges = self.graph.get_edge_data(pred_node, node) @@ -168,7 +168,7 @@ class OPGraph: callback function to determine the type constructor of the data encountered while updating the graph bounds. Defaults to get_type_constructor_python_constant_data. """ - node: ir.IntermediateNode + node: IntermediateNode for node in self.graph.nodes(): current_node_bounds = node_bounds[node] @@ -193,7 +193,7 @@ class OPGraph: data_type_constructor = max_data_type_constructor - if not isinstance(node, ir.Input): + if not isinstance(node, Input): for output_value in node.outputs: if isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer): output_value.data_type = make_integer_to_hold( @@ -242,9 +242,9 @@ class OPGraph: """Remove unreachable nodes from outputs.""" current_nodes = set(self.output_nodes.values()) - useful_nodes: Set[ir.IntermediateNode] = set() + useful_nodes: Set[IntermediateNode] = set() while current_nodes: - next_nodes: Set[ir.IntermediateNode] = set() + next_nodes: Set[IntermediateNode] = set() useful_nodes.update(current_nodes) for node in current_nodes: next_nodes.update(self.graph.pred[node]) diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index 7646c38b3..18a7f75e4 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -9,7 +9,7 @@ from ..data_types.floats import Float from ..data_types.integers import Integer from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph -from ..representation import intermediate as ir +from ..representation.intermediate import ArbitraryFunction, Constant, Input, IntermediateNode def fuse_float_operations( @@ -26,7 +26,7 @@ def fuse_float_operations( """ nx_graph = op_graph.graph - processed_terminal_nodes: Set[ir.IntermediateNode] = set() + processed_terminal_nodes: Set[IntermediateNode] = set() number_of_fuse = 0 while True: float_subgraph_search_result = find_float_subgraph_with_unique_terminal_node( @@ -56,7 +56,7 @@ def fuse_float_operations( if terminal_node in op_graph.output_nodes.values(): # Output value replace it # As the graph changes recreate the output_node_to_idx dict - output_node_to_idx: Dict[ir.IntermediateNode, List[int]] = { + output_node_to_idx: Dict[IntermediateNode, List[int]] = { out_node: [] for out_node in op_graph.output_nodes.values() } for output_idx, output_node in op_graph.output_nodes.items(): @@ -87,21 +87,21 @@ def fuse_float_operations( def convert_float_subgraph_to_fused_node( op_graph: OPGraph, - float_subgraph_start_nodes: Set[ir.IntermediateNode], - terminal_node: ir.IntermediateNode, - subgraph_all_nodes: Set[ir.IntermediateNode], -) -> Optional[Tuple[ir.ArbitraryFunction, ir.IntermediateNode]]: + float_subgraph_start_nodes: Set[IntermediateNode], + terminal_node: IntermediateNode, + subgraph_all_nodes: Set[IntermediateNode], +) -> Optional[Tuple[ArbitraryFunction, IntermediateNode]]: """Convert a float subgraph to an equivalent fused ArbitraryFunction node. Args: op_graph (OPGraph): The OPGraph the float subgraph is part of. - float_subgraph_start_nodes (Set[ir.IntermediateNode]): The nodes starting the float subgraph + float_subgraph_start_nodes (Set[IntermediateNode]): The nodes starting the float subgraph in `op_graph`. - terminal_node (ir.IntermediateNode): The node ending the float subgraph. - subgraph_all_nodes (Set[ir.IntermediateNode]): All the nodes in the float subgraph. + terminal_node (IntermediateNode): The node ending the float subgraph. + subgraph_all_nodes (Set[IntermediateNode]): All the nodes in the float subgraph. Returns: - Optional[Tuple[ir.ArbitraryFunction, ir.IntermediateNode]]: None if the float subgraph + Optional[Tuple[ArbitraryFunction, IntermediateNode]]: None if the float subgraph cannot be fused, otherwise returns a tuple containing the fused node and the node whose output must be plugged as the input to the subgraph. """ @@ -111,7 +111,7 @@ def convert_float_subgraph_to_fused_node( # Only one variable input node, find which node feeds its input non_constant_start_nodes = [ - node for node in float_subgraph_start_nodes if not isinstance(node, ir.Constant) + node for node in float_subgraph_start_nodes if not isinstance(node, Constant) ] custom_assert(len(non_constant_start_nodes) == 1) @@ -126,7 +126,7 @@ def convert_float_subgraph_to_fused_node( float_subgraph = nx.MultiDiGraph(nx_graph.subgraph(subgraph_all_nodes)) - new_subgraph_variable_input = ir.Input(new_input_value, "float_subgraph_input", 0) + new_subgraph_variable_input = Input(new_input_value, "float_subgraph_input", 0) float_subgraph.add_node(new_subgraph_variable_input) for node_after_input in nodes_after_input_set: @@ -155,7 +155,7 @@ def convert_float_subgraph_to_fused_node( ) # Create fused_node - fused_node = ir.ArbitraryFunction( + fused_node = ArbitraryFunction( deepcopy(new_subgraph_variable_input.inputs[0]), lambda x, float_op_subgraph, terminal_node: float_op_subgraph.evaluate({0: x})[ terminal_node @@ -176,8 +176,8 @@ def convert_float_subgraph_to_fused_node( def find_float_subgraph_with_unique_terminal_node( nx_graph: nx.MultiDiGraph, - processed_terminal_nodes: Set[ir.IntermediateNode], -) -> Optional[Tuple[Set[ir.IntermediateNode], ir.IntermediateNode, Set[ir.IntermediateNode]]]: + processed_terminal_nodes: Set[IntermediateNode], +) -> Optional[Tuple[Set[IntermediateNode], IntermediateNode, Set[IntermediateNode]]]: """Find a subgraph of the graph with float computations. The subgraph has a single terminal node with a single Integer output and has a single variable @@ -185,24 +185,24 @@ def find_float_subgraph_with_unique_terminal_node( Args: nx_graph (nx.MultiDiGraph): The networkx graph to search in. - processed_terminal_nodes (Set[ir.IntermediateNode]): The set of terminal nodes for which + processed_terminal_nodes (Set[IntermediateNode]): The set of terminal nodes for which subgraphs have already been searched, those will be skipped. Returns: - Optional[Tuple[Set[ir.IntermediateNode], ir.IntermediateNode, Set[ir.IntermediateNode]]]: + Optional[Tuple[Set[IntermediateNode], IntermediateNode, Set[IntermediateNode]]]: None if there are no float subgraphs to process in `nx_graph`. Otherwise returns a tuple containing the set of nodes beginning a float subgraph, the terminal node of the subgraph and the set of all the nodes in the subgraph. """ - def is_float_to_single_int_node(node: ir.IntermediateNode) -> bool: + def is_float_to_single_int_node(node: IntermediateNode) -> bool: return ( any(isinstance(input_.data_type, Float) for input_ in node.inputs) and len(node.outputs) == 1 and isinstance(node.outputs[0].data_type, Integer) ) - def single_int_output_node(node: ir.IntermediateNode) -> bool: + def single_int_output_node(node: IntermediateNode) -> bool: return len(node.outputs) == 1 and isinstance(node.outputs[0].data_type, Integer) float_subgraphs_terminal_nodes = ( @@ -211,7 +211,7 @@ def find_float_subgraph_with_unique_terminal_node( if is_float_to_single_int_node(node) and node not in processed_terminal_nodes ) - terminal_node: ir.IntermediateNode + terminal_node: IntermediateNode try: terminal_node = next(float_subgraphs_terminal_nodes) @@ -220,10 +220,10 @@ def find_float_subgraph_with_unique_terminal_node( # Use dict as ordered set current_nodes = {terminal_node: None} - float_subgraph_start_nodes: Set[ir.IntermediateNode] = set() - subgraph_all_nodes: Set[ir.IntermediateNode] = set() + float_subgraph_start_nodes: Set[IntermediateNode] = set() + subgraph_all_nodes: Set[IntermediateNode] = set() while current_nodes: - next_nodes: Dict[ir.IntermediateNode, None] = {} + next_nodes: Dict[IntermediateNode, None] = {} for node in current_nodes: subgraph_all_nodes.add(node) predecessors = nx_graph.pred[node] @@ -240,16 +240,16 @@ def find_float_subgraph_with_unique_terminal_node( def subgraph_has_unique_variable_input( - float_subgraph_start_nodes: Set[ir.IntermediateNode], + float_subgraph_start_nodes: Set[IntermediateNode], ) -> bool: """Check that only one of the nodes starting the subgraph is variable. Args: - float_subgraph_start_nodes (Set[ir.IntermediateNode]): The nodes starting the subgraph. + float_subgraph_start_nodes (Set[IntermediateNode]): The nodes starting the subgraph. Returns: - bool: True if only one of the nodes is not an ir.Constant + bool: True if only one of the nodes is not an Constant """ # Only one input to the subgraph where computations are done in floats is variable, this # is the only case we can manage with ArbitraryFunction fusing - return sum(not isinstance(node, ir.Constant) for node in float_subgraph_start_nodes) == 1 + return sum(not isinstance(node, Constant) for node in float_subgraph_start_nodes) == 1 diff --git a/concrete/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py index 094c8f80d..1bd9ad747 100644 --- a/concrete/common/tracing/base_tracer.py +++ b/concrete/common/tracing/base_tracer.py @@ -4,8 +4,13 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Iterable, List, Tuple, Type, Union from ..debugging.custom_assert import custom_assert -from ..representation import intermediate as ir -from ..representation.intermediate import IR_MIX_VALUES_FUNC_ARG_NAME +from ..representation.intermediate import ( + IR_MIX_VALUES_FUNC_ARG_NAME, + Add, + IntermediateNode, + Mul, + Sub, +) from ..values import BaseValue @@ -13,14 +18,14 @@ class BaseTracer(ABC): """Base class for implementing tracers.""" inputs: List["BaseTracer"] - traced_computation: ir.IntermediateNode + traced_computation: IntermediateNode output: BaseValue _mix_values_func: Callable[..., BaseValue] def __init__( self, inputs: Iterable["BaseTracer"], - traced_computation: ir.IntermediateNode, + traced_computation: IntermediateNode, output_index: int, ) -> None: self.inputs = list(inputs) @@ -62,14 +67,14 @@ class BaseTracer(ABC): def instantiate_output_tracers( self, inputs: Iterable[Union["BaseTracer", Any]], - computation_to_trace: Type[ir.IntermediateNode], + computation_to_trace: Type[IntermediateNode], ) -> Tuple["BaseTracer", ...]: """Instantiate all output BaseTracer for a given computation. Args: inputs (Iterable[Union[BaseTracer, Any]]): Previous BaseTracer or data used as inputs for a new node. - computation_to_trace (Type[ir.IntermediateNode]): The IntermediateNode class + computation_to_trace (Type[IntermediateNode]): The IntermediateNode class to instantiate for the computation being traced Returns: @@ -103,7 +108,7 @@ class BaseTracer(ABC): result_tracer = self.instantiate_output_tracers( [self, other], - ir.Add, + Add, ) custom_assert(len(result_tracer) == 1) @@ -120,7 +125,7 @@ class BaseTracer(ABC): result_tracer = self.instantiate_output_tracers( [self, other], - ir.Sub, + Sub, ) custom_assert(len(result_tracer) == 1) @@ -132,7 +137,7 @@ class BaseTracer(ABC): result_tracer = self.instantiate_output_tracers( [other, self], - ir.Sub, + Sub, ) custom_assert(len(result_tracer) == 1) @@ -144,7 +149,7 @@ class BaseTracer(ABC): result_tracer = self.instantiate_output_tracers( [self, other], - ir.Mul, + Mul, ) custom_assert(len(result_tracer) == 1) diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index 712926270..a2c894312 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -7,7 +7,7 @@ import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph from ..debugging.custom_assert import custom_assert -from ..representation import intermediate as ir +from ..representation.intermediate import Input from ..values import BaseValue from .base_tracer import BaseTracer @@ -50,7 +50,7 @@ def make_input_tracer( Returns: BaseTracer: The BaseTracer for that input value """ - return tracer_class([], ir.Input(input_value, input_name, input_idx), 0) + return tracer_class([], Input(input_value, input_name, input_idx), 0) def prepare_function_parameters( diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index ea4d3925e..fd1074083 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -19,7 +19,7 @@ from ..common.mlir.utils import ( ) from ..common.operator_graph import OPGraph from ..common.optimization.topological import fuse_float_operations -from ..common.representation import intermediate as ir +from ..common.representation.intermediate import IntermediateNode from ..common.values import BaseValue from ..numpy.tracing import trace_numpy_function from .np_dtypes_helpers import ( @@ -99,7 +99,7 @@ def _compile_numpy_function_into_op_graph_internal( fuse_float_operations(op_graph, compilation_artifacts) # TODO: To be removed once we support more than integers - offending_non_integer_nodes: List[ir.IntermediateNode] = [] + offending_non_integer_nodes: List[IntermediateNode] = [] op_grap_is_int_prog = check_op_graph_is_integer_program(op_graph, offending_non_integer_nodes) if not op_grap_is_int_prog: raise ValueError( From e4a06116ecb093be73505620c6669ea51a555729 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 23 Sep 2021 15:50:15 +0300 Subject: [PATCH 0311/1104] fix: inputset of test_mlir_converter for dot operation --- tests/common/mlir/test_mlir_converter.py | 33 ++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index fec21c6e1..2ab61b91a 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -213,13 +213,28 @@ def datagen(*args): }, (range(0, 8),), ), + ], +) +def test_mlir_converter(func, args_dict, args_ranges): + """Test the conversion to MLIR by calling the parser from the compiler""" + inputset = datagen(*args_ranges) + result_graph = compile_numpy_function_into_op_graph(func, args_dict, inputset) + converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + mlir_result = converter.convert(result_graph) + # testing that this doesn't raise an error + compiler.round_trip(mlir_result) + + +@pytest.mark.parametrize( + "func, args_dict, args_ranges", + [ ( dot, { "x": EncryptedTensor(Integer(64, is_signed=False), shape=(4,)), "y": ClearTensor(Integer(64, is_signed=False), shape=(4,)), }, - (range(0, 8), range(0, 8)), + (range(0, 4), range(0, 4)), ), ( dot, @@ -227,14 +242,22 @@ def datagen(*args): "x": ClearTensor(Integer(64, is_signed=False), shape=(4,)), "y": EncryptedTensor(Integer(64, is_signed=False), shape=(4,)), }, - (range(0, 8), range(0, 8)), + (range(0, 4), range(0, 4)), ), ], ) -def test_mlir_converter(func, args_dict, args_ranges): +def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): """Test the conversion to MLIR by calling the parser from the compiler""" - inputset = datagen(*args_ranges) - result_graph = compile_numpy_function_into_op_graph(func, args_dict, inputset) + assert len(args_dict["x"].shape) == 1 + assert len(args_dict["y"].shape) == 1 + + n = args_dict["x"].shape[0] + + result_graph = compile_numpy_function_into_op_graph( + func, + args_dict, + (([data[0]] * n, [data[1]] * n) for data in datagen(*args_ranges)), + ) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error From eaf8cfb933e220fe0b466f59652af6693a322aa2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 23 Sep 2021 14:44:15 +0200 Subject: [PATCH 0312/1104] tests: add function for ArbitraryFunction arbitrary_func equivalence - this is not perfect but pretty close to the best we can do --- tests/common/extensions/test_table.py | 10 ++++++++-- tests/conftest.py | 28 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index 81e7e90ca..cdd0481a2 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -54,13 +54,16 @@ def test_lookup_table_encrypted_lookup(test_helpers): input_x = ir.Input(input_value=x, input_name="x", program_input_idx=0) ref_graph.add_node(input_x) + # pylint: disable=protected-access + # Need access to _checked_indexing to have is_equivalent_to work for ir.ArbitraryFunction output_arbitrary_function = ir.ArbitraryFunction( input_base_value=x, - arbitrary_func=lambda x, table: table[x], + arbitrary_func=LookupTable._checked_indexing, output_dtype=table.output_dtype, op_kwargs={"table": deepcopy(table.table)}, op_name="TLU", ) + # pylint: enable=protected-access ref_graph.add_node(output_arbitrary_function) ref_graph.add_edge(input_x, output_arbitrary_function, input_idx=0) @@ -91,13 +94,16 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): input_x = ir.Input(input_value=x, input_name="x", program_input_idx=0) ref_graph.add_node(input_x) + # pylint: disable=protected-access + # Need access to _checked_indexing to have is_equivalent_to work for ir.ArbitraryFunction intermediate_arbitrary_function = ir.ArbitraryFunction( input_base_value=x, - arbitrary_func=lambda x, table: table[x], + arbitrary_func=LookupTable._checked_indexing, output_dtype=table.output_dtype, op_kwargs={"table": deepcopy(table.table)}, op_name="TLU", ) + # pylint: enable=protected-access ref_graph.add_node(intermediate_arbitrary_function) constant_3 = ir.Constant(3) diff --git a/tests/conftest.py b/tests/conftest.py index 9df9f064c..09d84a55f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """PyTest configuration file""" +import operator from typing import Callable, Dict, Type import networkx as nx @@ -39,10 +40,37 @@ def is_equivalent_add(lhs: Add, rhs: object) -> bool: return _is_equivalent_to_binary_commutative(lhs, rhs) +# From https://stackoverflow.com/a/28635464 +_code_and_constants_attr_getter = operator.attrgetter("co_code", "co_consts") + + +def _code_and_constants(object_): + """Helper function to get python code and constants""" + return _code_and_constants_attr_getter(object_.__code__) + + +def python_functions_are_equal_or_equivalent(lhs: object, rhs: object) -> bool: + """Helper function to check if two functions are equal or their code are equivalent. + + This is not perfect, but will be good enough for tests. + """ + + if lhs == rhs: + return True + + try: + lhs_code_and_constants = _code_and_constants(lhs) + rhs_code_and_constants = _code_and_constants(rhs) + return lhs_code_and_constants == rhs_code_and_constants + except AttributeError: + return False + + def is_equivalent_arbitrary_function(lhs: ArbitraryFunction, rhs: object) -> bool: """Helper function to check if an ArbitraryFunction node is equivalent to an other object.""" return ( isinstance(rhs, ArbitraryFunction) + and python_functions_are_equal_or_equivalent(lhs.arbitrary_func, rhs.arbitrary_func) and lhs.op_args == rhs.op_args and lhs.op_kwargs == rhs.op_kwargs and lhs.op_name == rhs.op_name From 0061e01d62db5bc75998cb85ab1a3084cbdd1853 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 22 Sep 2021 12:10:11 +0300 Subject: [PATCH 0313/1104] feat: implement checking coherence between inputset and parameters --- concrete/common/__init__.py | 2 +- .../bounds_measurement/inputset_eval.py | 127 ++++++++++++++- concrete/common/compilation/configuration.py | 3 + concrete/common/data_types/dtypes_helpers.py | 67 ++++++-- concrete/common/values/scalars.py | 11 ++ concrete/numpy/compile.py | 5 +- concrete/numpy/np_dtypes_helpers.py | 24 ++- .../bounds_measurement/test_inputset_eval.py | 146 +++++++++++++++++- .../common/data_types/test_dtypes_helpers.py | 1 - tests/numpy/test_compile.py | 2 +- 10 files changed, 360 insertions(+), 28 deletions(-) diff --git a/concrete/common/__init__.py b/concrete/common/__init__.py index 0dcf40159..ce8e30ea3 100644 --- a/concrete/common/__init__.py +++ b/concrete/common/__init__.py @@ -1,3 +1,3 @@ """Module for shared data structures and code.""" -from . import compilation, data_types, debugging, representation +from . import compilation, data_types, debugging, representation, values from .common_helpers import check_op_graph_is_integer_program, is_a_power_of_2 diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index f285de2be..4be083f5f 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -1,17 +1,105 @@ """Code to evaluate the IR graph on inputsets.""" +import sys from typing import Any, Callable, Dict, Iterable, Tuple +from ..compilation import CompilationConfiguration +from ..data_types.dtypes_helpers import ( + get_base_value_for_python_constant_data, + is_data_type_compatible_with, +) from ..debugging import custom_assert from ..operator_graph import OPGraph from ..representation.intermediate import IntermediateNode +def _check_input_coherency( + input_to_check: Dict[str, Any], + parameters: Dict[str, Any], + get_base_value_for_constant_data_func: Callable[[Any], Any], +): + """Check whether `input_to_check` is coherent with `parameters`. + + This function works by iterating over each constant of the input, + determining base value of the constant using `get_base_value_for_constant_data_func` and + checking if the base value of the contant is compatible with the base value of the parameter. + + Args: + input_to_check (Dict[str, Any]): input to check coherency of + parameters (Dict[str, Any]): parameters and their expected base values + get_base_value_for_constant_data_func (Callable[[Any], Any]): + function to get the base value of python objects. + + Returns: + List[str]: List of warnings about the coherency + """ + + warnings = [] + for parameter_name, value in input_to_check.items(): + parameter_base_value = parameters[parameter_name] + + base_value_class = get_base_value_for_constant_data_func(value) + base_value = base_value_class(is_encrypted=parameter_base_value.is_encrypted) + + if base_value.shape != parameter_base_value.shape or not is_data_type_compatible_with( + base_value.data_type, parameter_base_value.data_type + ): + warnings.append( + f"expected {str(parameter_base_value)} " + f"for parameter `{parameter_name}` " + f"but got {str(base_value)} " + f"which is not compatible" + ) + return warnings + + +def _print_input_coherency_warnings( + current_input_index: int, + current_input_data: Dict[int, Any], + parameters: Dict[str, Any], + parameter_index_to_parameter_name: Dict[int, str], + get_base_value_for_constant_data_func: Callable[[Any], Any], +): + """Print coherency warning for `input_to_check` against `parameters`. + + Args: + current_input_index (int): index of the current input on the inputset + current_input_data (Dict[int, Any]): input to print coherency warnings of + parameters (Dict[str, Any]): parameters and their expected base values + parameter_index_to_parameter_name (Dict[int, str]): + dict to get parameter names from parameter indices + get_base_value_for_constant_data_func (Callable[[Any], Any]): + function to get the base value of python objects. + + Returns: + None + """ + + current_input_named_data = { + parameter_index_to_parameter_name[index]: data for index, data in current_input_data.items() + } + + problems = _check_input_coherency( + current_input_named_data, + parameters, + get_base_value_for_constant_data_func, + ) + for problem in problems: + sys.stderr.write( + f"Warning: Input #{current_input_index} (0-indexed) " + f"is not coherent with the hinted parameters ({problem})\n", + ) + + def eval_op_graph_bounds_on_inputset( op_graph: OPGraph, inputset: Iterable[Tuple[Any, ...]], + compilation_configuration: CompilationConfiguration, min_func: Callable[[Any, Any], Any] = min, max_func: Callable[[Any, Any], Any] = max, + get_base_value_for_constant_data_func: Callable[ + [Any], Any + ] = get_base_value_for_python_constant_data, ) -> Tuple[int, Dict[IntermediateNode, Dict[str, Any]]]: """Evaluate the bounds with a inputset. @@ -23,12 +111,16 @@ def eval_op_graph_bounds_on_inputset( inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It needs to be an iterable on tuples which are of the same length than the number of parameters in the function, and in the same order than these same parameters + compilation_configuration (CompilationConfiguration): Configuration object to use + during determining input checking strategy min_func (Callable[[Any, Any], Any], optional): custom function to compute a scalar minimum between two values that can be encountered during evaluation (for e.g. numpy or torch tensors). Defaults to min. max_func (Callable[[Any, Any], Any], optional): custom function to compute a scalar maximum between two values that can be encountered during evaluation (for e.g. numpy or torch tensors). Defaults to max. + get_base_value_for_constant_data_func (Callable[[Any], Any], optional): custom function + to compute the base value of a python object. Returns: Tuple[int, Dict[IntermediateNode, Dict[str, Any]]]: number of inputs in the inputset and @@ -49,14 +141,29 @@ def eval_op_graph_bounds_on_inputset( # TODO: do we want to check coherence between the input data type and the corresponding Input ir # node expected data type ? Not considering bit_width as they may not make sense at this stage - inputset_size = 0 - inputset_iterator = iter(inputset) + parameter_index_to_parameter_name = { + index: input_node.input_name for index, input_node in op_graph.input_nodes.items() + } + parameters = { + input_node.input_name: input_node.inputs[0] for input_node in op_graph.input_nodes.values() + } - first_input_data = dict(enumerate(next(inputset_iterator))) + inputset_iterator = iter(inputset) + inputset_size = 0 + + current_input_data = dict(enumerate(next(inputset_iterator))) inputset_size += 1 - check_inputset_input_len_is_valid(first_input_data.values()) - first_output = op_graph.evaluate(first_input_data) + check_inputset_input_len_is_valid(current_input_data.values()) + _print_input_coherency_warnings( + inputset_size - 1, + current_input_data, + parameters, + parameter_index_to_parameter_name, + get_base_value_for_constant_data_func, + ) + + first_output = op_graph.evaluate(current_input_data) # We evaluate the min and max func to be able to resolve the tensors min and max rather than # having the tensor itself as the stored min and max values. @@ -68,7 +175,17 @@ def eval_op_graph_bounds_on_inputset( for input_data in inputset_iterator: inputset_size += 1 current_input_data = dict(enumerate(input_data)) + check_inputset_input_len_is_valid(current_input_data.values()) + if compilation_configuration.check_every_input_in_inputset: + _print_input_coherency_warnings( + inputset_size - 1, + current_input_data, + parameters, + parameter_index_to_parameter_name, + get_base_value_for_constant_data_func, + ) + current_output = op_graph.evaluate(current_input_data) for node, value in current_output.items(): node_bounds[node]["min"] = min_func(node_bounds[node]["min"], value) diff --git a/concrete/common/compilation/configuration.py b/concrete/common/compilation/configuration.py index c600698e6..07f909e6d 100644 --- a/concrete/common/compilation/configuration.py +++ b/concrete/common/compilation/configuration.py @@ -6,11 +6,14 @@ class CompilationConfiguration: dump_artifacts_on_unexpected_failures: bool enable_topological_optimizations: bool + check_every_input_in_inputset: bool def __init__( self, dump_artifacts_on_unexpected_failures: bool = True, enable_topological_optimizations: bool = True, + check_every_input_in_inputset: bool = False, ): self.dump_artifacts_on_unexpected_failures = dump_artifacts_on_unexpected_failures self.enable_topological_optimizations = enable_topological_optimizations + self.check_every_input_in_inputset = check_every_input_in_inputset diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 49c096384..8f5ed10a9 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial -from typing import Callable, Union, cast +from typing import Callable, List, Union, cast from ..debugging.custom_assert import custom_assert from ..values import ( @@ -306,7 +306,9 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> ) -def get_base_data_type_for_python_constant_data(constant_data: Union[int, float]) -> BaseDataType: +def get_base_data_type_for_python_constant_data( + constant_data: Union[int, float, List[int], List[float]] +) -> BaseDataType: """Determine the BaseDataType to hold the input constant data. Args: @@ -318,10 +320,17 @@ def get_base_data_type_for_python_constant_data(constant_data: Union[int, float] """ constant_data_type: BaseDataType custom_assert( - isinstance(constant_data, (int, float)), + isinstance(constant_data, (int, float, list)), f"Unsupported constant data of type {type(constant_data)}", ) - if isinstance(constant_data, int): + + if isinstance(constant_data, list): + custom_assert(len(constant_data) > 0, "Data type of empty list cannot be detected") + constant_data_type = get_base_data_type_for_python_constant_data(constant_data[0]) + for value in constant_data: + other_data_type = get_base_data_type_for_python_constant_data(value) + constant_data_type = find_type_to_hold_both_lossy(constant_data_type, other_data_type) + elif isinstance(constant_data, int): is_signed = constant_data < 0 constant_data_type = Integer( get_bits_to_represent_value_as_integer(constant_data, is_signed), is_signed @@ -332,22 +341,29 @@ def get_base_data_type_for_python_constant_data(constant_data: Union[int, float] def get_base_value_for_python_constant_data( - constant_data: Union[int, float] -) -> Callable[..., ScalarValue]: - """Wrap the BaseDataType to hold the input constant data in a ScalarValue partial. + constant_data: Union[int, float, List[int], List[float]] +) -> Callable[..., BaseValue]: + """Wrap the BaseDataType to hold the input constant data in BaseValue partial. - The returned object can then be instantiated as an Encrypted or Clear version of the ScalarValue - by calling it with the proper arguments forwarded to the ScalarValue `__init__` function + The returned object can then be instantiated as an Encrypted or Clear version + by calling it with the proper arguments forwarded to the BaseValue `__init__` function Args: - constant_data (Union[int, float]): The constant data for which to determine the - corresponding ScalarValue and BaseDataType. + constant_data (Union[int, float, List[int], List[float]]): The constant data + for which to determine the corresponding Value. Returns: - Callable[..., ScalarValue]: A partial object that will return the proper ScalarValue when - called with `encrypted` as keyword argument (forwarded to the ScalarValue `__init__` + Callable[..., BaseValue]: A partial object that will return the proper BaseValue when + called with `is_encrypted` as keyword argument (forwarded to the BaseValue `__init__` method). """ + + if isinstance(constant_data, list): + assert len(constant_data) > 0 + constant_shape = (len(constant_data),) + constant_data_type = get_base_data_type_for_python_constant_data(constant_data) + return partial(TensorValue, data_type=constant_data_type, shape=constant_shape) + constant_data_type = get_base_data_type_for_python_constant_data(constant_data) return partial(ScalarValue, data_type=constant_data_type) @@ -359,3 +375,28 @@ def get_type_constructor_for_python_constant_data(constant_data: Union[int, floa constant_data (Any): The data for which we want to determine the type constructor. """ return type(constant_data) + + +def is_data_type_compatible_with( + dtype: BaseDataType, + other: BaseDataType, +) -> bool: + """Determine whether dtype is compatible with other. + + `dtype` being compatible with `other` means `other` can hold every value of `dtype` + (e.g., uint2 is compatible with uint4 and int4) + (e.g., int2 is compatible with int4 but not with uint4) + + Note that this function is not symetric. + (e.g., uint2 is compatible with uint4, but uint4 is not compatible with uint2) + + Args: + dtype (BaseDataType): dtype to check compatiblity + other (BaseDataType): dtype to check compatiblity against + + Returns: + bool: Whether the dtype is compatible with other or not + """ + + combination = find_type_to_hold_both_lossy(dtype, other) + return other == combination diff --git a/concrete/common/values/scalars.py b/concrete/common/values/scalars.py index 1b057fa99..e1b541e6d 100644 --- a/concrete/common/values/scalars.py +++ b/concrete/common/values/scalars.py @@ -1,5 +1,7 @@ """Module that defines the scalar values in a program.""" +from typing import Tuple + from ..data_types.base import BaseDataType from .base import BaseValue @@ -14,6 +16,15 @@ class ScalarValue(BaseValue): encrypted_str = "Encrypted" if self._is_encrypted else "Clear" return f"{encrypted_str}Scalar<{self.data_type!r}>" + @property + def shape(self) -> Tuple[int, ...]: + """Return the ScalarValue shape property. + + Returns: + Tuple[int, ...]: The ScalarValue shape which is `()`. + """ + return () + def make_clear_scalar(data_type: BaseDataType) -> ScalarValue: """Create a clear ScalarValue. diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index fd1074083..a00cbc030 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -24,6 +24,7 @@ from ..common.values import BaseValue from ..numpy.tracing import trace_numpy_function from .np_dtypes_helpers import ( get_base_data_type_for_numpy_or_python_constant_data, + get_base_value_for_numpy_or_python_constant_data, get_type_constructor_for_numpy_or_python_constant_data, ) @@ -112,8 +113,10 @@ def _compile_numpy_function_into_op_graph_internal( inputset_size, node_bounds = eval_op_graph_bounds_on_inputset( op_graph, inputset, + compilation_configuration=compilation_configuration, min_func=numpy_min_func, max_func=numpy_max_func, + get_base_value_for_constant_data_func=get_base_value_for_numpy_or_python_constant_data, ) # Check inputset size @@ -134,7 +137,7 @@ def _compile_numpy_function_into_op_graph_internal( minimum_required_inputset_size = min(inputset_size_upper_limit, 10) if inputset_size < minimum_required_inputset_size: sys.stderr.write( - f"Provided inputset contains too few inputs " + f"Warning: Provided inputset contains too few inputs " f"(it should have had at least {minimum_required_inputset_size} " f"but it only had {inputset_size})\n" ) diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 2d2c4eab7..cbcdb939d 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -10,6 +10,7 @@ from numpy.typing import DTypeLike from ..common.data_types.base import BaseDataType from ..common.data_types.dtypes_helpers import ( BASE_DATA_TYPES, + find_type_to_hold_both_lossy, get_base_data_type_for_python_constant_data, get_base_value_for_python_constant_data, get_type_constructor_for_python_constant_data, @@ -116,12 +117,26 @@ def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> """ base_dtype: BaseDataType custom_assert( - isinstance(constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)), + isinstance( + constant_data, (int, float, list, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) + ), f"Unsupported constant data of type {type(constant_data)}", ) if isinstance(constant_data, (numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)): + native_type = ( + float + if constant_data.dtype == numpy.float32 or constant_data.dtype == numpy.float64 + else int + ) + + min_value = native_type(constant_data.min()) + max_value = native_type(constant_data.max()) + + min_value_dtype = get_base_data_type_for_python_constant_data(min_value) + max_value_dtype = get_base_data_type_for_python_constant_data(max_value) + # numpy - base_dtype = convert_numpy_dtype_to_base_data_type(constant_data.dtype) + base_dtype = find_type_to_hold_both_lossy(min_value_dtype, max_value_dtype) else: # python base_dtype = get_base_data_type_for_python_constant_data(constant_data) @@ -148,7 +163,10 @@ def get_base_value_for_numpy_or_python_constant_data( """ constant_data_value: Callable[..., BaseValue] custom_assert( - isinstance(constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)), + isinstance( + constant_data, + (int, float, list, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES), + ), f"Unsupported constant data of type {type(constant_data)}", ) diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index 3569c8bc5..dc3c3d7dc 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -2,12 +2,16 @@ from typing import Tuple +import numpy as np import pytest from concrete.common.bounds_measurement.inputset_eval import eval_op_graph_bounds_on_inputset +from concrete.common.compilation import CompilationConfiguration from concrete.common.data_types.floats import Float -from concrete.common.data_types.integers import Integer -from concrete.common.values import EncryptedScalar +from concrete.common.data_types.integers import Integer, UnsignedInteger +from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor +from concrete.numpy.compile import numpy_max_func, numpy_min_func +from concrete.numpy.np_dtypes_helpers import get_base_value_for_numpy_or_python_constant_data from concrete.numpy.tracing import trace_numpy_function @@ -280,7 +284,9 @@ def test_eval_op_graph_bounds_on_inputset_multiple_output( yield (x_gen, y_gen) _, node_bounds = eval_op_graph_bounds_on_inputset( - op_graph, data_gen(*tuple(range(x[0], x[1] + 1) for x in input_ranges)) + op_graph, + data_gen(*tuple(range(x[0], x[1] + 1) for x in input_ranges)), + CompilationConfiguration(), ) for i, output_node in op_graph.output_nodes.items(): @@ -291,3 +297,137 @@ def test_eval_op_graph_bounds_on_inputset_multiple_output( for i, output_node in op_graph.output_nodes.items(): assert expected_output_data_type[i] == output_node.outputs[0].data_type + + +def test_eval_op_graph_bounds_on_non_conformant_inputset_default(capsys): + """Test function for eval_op_graph_bounds_on_inputset with non conformant inputset""" + + def f(x, y): + return np.dot(x, y) + + x = EncryptedTensor(UnsignedInteger(2), (3,)) + y = ClearTensor(UnsignedInteger(2), (3,)) + + inputset = [ + ([2, 1, 3, 1], [1, 2, 1, 1]), + ([3, 3, 3], [3, 3, 5]), + ] + + op_graph = trace_numpy_function(f, {"x": x, "y": y}) + + configuration = CompilationConfiguration() + eval_op_graph_bounds_on_inputset(op_graph, inputset, compilation_configuration=configuration) + + captured = capsys.readouterr() + assert ( + captured.err == "Warning: Input #0 (0-indexed) is not coherent with the hinted parameters " + "(expected EncryptedTensor, shape=(3,)> for parameter `x` " + "but got EncryptedTensor, shape=(4,)> which is not compatible)\n" + "Warning: Input #0 (0-indexed) is not coherent with the hinted parameters " + "(expected ClearTensor, shape=(3,)> for parameter `y` " + "but got ClearTensor, shape=(4,)> which is not compatible)\n" + ) + + +def test_eval_op_graph_bounds_on_non_conformant_inputset_check_all(capsys): + """Test function for eval_op_graph_bounds_on_inputset with non conformant inputset, check all""" + + def f(x, y): + return np.dot(x, y) + + x = EncryptedTensor(UnsignedInteger(2), (3,)) + y = ClearTensor(UnsignedInteger(2), (3,)) + + inputset = [ + ([2, 1, 3, 1], [1, 2, 1, 1]), + ([3, 3, 3], [3, 3, 5]), + ] + + op_graph = trace_numpy_function(f, {"x": x, "y": y}) + + configuration = CompilationConfiguration(check_every_input_in_inputset=True) + eval_op_graph_bounds_on_inputset(op_graph, inputset, compilation_configuration=configuration) + + captured = capsys.readouterr() + assert ( + captured.err == "Warning: Input #0 (0-indexed) is not coherent with the hinted parameters " + "(expected EncryptedTensor, shape=(3,)> for parameter `x` " + "but got EncryptedTensor, shape=(4,)> which is not compatible)\n" + "Warning: Input #0 (0-indexed) is not coherent with the hinted parameters " + "(expected ClearTensor, shape=(3,)> for parameter `y` " + "but got ClearTensor, shape=(4,)> which is not compatible)\n" + "Warning: Input #1 (0-indexed) is not coherent with the hinted parameters " + "(expected ClearTensor, shape=(3,)> for parameter `y` " + "but got ClearTensor, shape=(3,)> which is not compatible)\n" + ) + + +def test_eval_op_graph_bounds_on_conformant_numpy_inputset_check_all(capsys): + """Test function for eval_op_graph_bounds_on_inputset + with conformant inputset of numpy arrays, check all""" + + def f(x, y): + return np.dot(x, y) + + x = EncryptedTensor(UnsignedInteger(2), (3,)) + y = ClearTensor(UnsignedInteger(2), (3,)) + + inputset = [ + (np.array([2, 1, 3]), np.array([1, 2, 1])), + (np.array([3, 3, 3]), np.array([3, 3, 1])), + ] + + op_graph = trace_numpy_function(f, {"x": x, "y": y}) + + configuration = CompilationConfiguration(check_every_input_in_inputset=True) + eval_op_graph_bounds_on_inputset( + op_graph, + inputset, + compilation_configuration=configuration, + min_func=numpy_min_func, + max_func=numpy_max_func, + get_base_value_for_constant_data_func=get_base_value_for_numpy_or_python_constant_data, + ) + + captured = capsys.readouterr() + assert captured.err == "" + + +def test_eval_op_graph_bounds_on_non_conformant_numpy_inputset_check_all(capsys): + """Test function for eval_op_graph_bounds_on_inputset with non conformant inputset, check all""" + + def f(x, y): + return np.dot(x, y) + + x = EncryptedTensor(UnsignedInteger(2), (3,)) + y = ClearTensor(UnsignedInteger(2), (3,)) + + inputset = [ + (np.array([2, 1, 3, 1]), np.array([1, 2, 1, 1])), + (np.array([3, 3, 3]), np.array([3, 3, 5])), + ] + + op_graph = trace_numpy_function(f, {"x": x, "y": y}) + + configuration = CompilationConfiguration(check_every_input_in_inputset=True) + eval_op_graph_bounds_on_inputset( + op_graph, + inputset, + compilation_configuration=configuration, + min_func=numpy_min_func, + max_func=numpy_max_func, + get_base_value_for_constant_data_func=get_base_value_for_numpy_or_python_constant_data, + ) + + captured = capsys.readouterr() + assert ( + captured.err == "Warning: Input #0 (0-indexed) is not coherent with the hinted parameters " + "(expected EncryptedTensor, shape=(3,)> for parameter `x` " + "but got EncryptedTensor, shape=(4,)> which is not compatible)\n" + "Warning: Input #0 (0-indexed) is not coherent with the hinted parameters " + "(expected ClearTensor, shape=(3,)> for parameter `y` " + "but got ClearTensor, shape=(4,)> which is not compatible)\n" + "Warning: Input #1 (0-indexed) is not coherent with the hinted parameters " + "(expected ClearTensor, shape=(3,)> for parameter `y` " + "but got ClearTensor, shape=(3,)> which is not compatible)\n" + ) diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 74413738d..5a0d52c9a 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -1,5 +1,4 @@ """Test file for data types helpers""" - import pytest from concrete.common.data_types.base import BaseDataType diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 3aacdb737..1ef3cd0c3 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -303,7 +303,7 @@ def test_compile_function_with_dot(function, params, shape, ref_graph_str): iter_i = itertools.product(range(0, max_for_ij + 1), repeat=repeat) iter_j = itertools.product(range(0, max_for_ij + 1), repeat=repeat) for prod_i, prod_j in itertools.product(iter_i, iter_j): - yield (prod_i, prod_j) + yield (list(prod_i), list(prod_j)) max_for_ij = 3 assert len(shape) == 1 From a386532c19f5901538f081655fc64cfa7e194a81 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Wed, 22 Sep 2021 14:04:59 +0200 Subject: [PATCH 0314/1104] docs: update theme --- docs/_static/css/zama.css | 191 ++++++++++++++++++++++++-- docs/_templates/layout.html | 260 ++++++++++++++++++++++++++++++++++++ docs/conf.py | 5 +- poetry.lock | 21 ++- pyproject.toml | 1 + 5 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 docs/_templates/layout.html diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index 70ac7c69e..693cf8fd3 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -3,12 +3,78 @@ /* This line is theme specific - it includes the base theme CSS */ @import 'theme.css'; /* for the Read the Docs theme */ -.wy-side-nav-search { - background-color: #ffd208; +:root { + /* primary: yellow, secondary: orange, tertiary: black */ + --primary-color: #ffd208; + --primary-color-darker: #cba706; + --secondary-color: #ffb854; + --secondary-color-light: #fff0d9; + --tertiary-color: #414042; + --tertiary-color-light: #e6e7e8; + --link-color: var(--primary-color-darker); + --primary-font: Telegraf, sans-serif; } -.wy-menu-vertical header, .wy-menu-vertical p.caption { - color: #ffd208; +.zama-header { + position: fixed; + left: 0; + right: 0; + z-index: 201; + height: 43px; + background: black; + color: white; + display: flex; + align-items: center; + padding: 0 40px; + justify-content: flex-end; + font-size: 16px; + box-shadow: 0 0 0 1px rgba(0,0,0,0.4); +} + +.zama-header > a { + display: flex; + height: 33px; + align-items: center; + color: #fff; + font-weight: bold ; + font-family: var(--primary-font); + text-decoration: none; + margin-left: 30px; +} + +.zama-header > a:hover, +.zama-header > a:focus, +.zama-header > a:active { + color: var(--primary-color); + text-decoration: none; +} + +body { + font-family: var(--primary-font); +} + +.rst-content .toctree-wrapper>p.caption, h1, h2, h3, h4, h5, h6, legend { + font-family: var(--primary-font); +} + +input[type=color], input[type=date], input[type=datetime-local], input[type=datetime], input[type=email], input[type=month], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=text], input[type=time], input[type=url], input[type=week] { + font-family: var(--primary-font); +} +.wy-side-nav-search input[type=text]{ + border-color: var(--primary-color); +} + +.wy-side-nav-search { + background-color: var(--primary-color); +} + +.wy-menu-vertical { + padding-bottom: 30px; +} + +.wy-menu-vertical header, +.wy-menu-vertical p.caption { + color: var(--primary-color); } .wy-side-nav-search > a { @@ -28,19 +94,124 @@ padding: 0; /* Adjust this to change the adjustment of the Zama's logo, on the top left */ - margin: 0 0 0 -80px !important; + margin: 0 0 0 -88px !important; +} +.wy-grid-for-nav { + padding-top: 43px; } -.rst-content code.literal, .rst-content tt.literal { - color: #ffd208; +.wy-nav-side { + top: 43px; +} + +.rst-content code.literal, +.rst-content tt.literal { + color: var(--primary-color); background-color: #696969; } +.wy-nav-content a { + color: var(--link-color); + text-decoration: none; +} + +.wy-nav-content a:hover, +.wy-nav-content a:focus, +.wy-nav-content a:active { + color: var(--link-color); + text-decoration: underline; +} + +.wy-nav-content a.btn:hover, +.wy-nav-content a.btn:focus, +.wy-nav-content a.btn:active{ + color: black; + text-decoration: none; +} + .wy-nav-top { color: #696969; background: #343131 } - -.wy-nav-top > a { - color: #ffd208; +.wy-nav-top a { + color:var(--primary-color); +} + +html.writer-html4 .rst-content dl:not(.docutils)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt { + display: table; + margin: 6px 0; + font-size: 90%; + line-height: normal; + background: var(--tertiary-color-light); + color: var(--tertiary-color); + border-top: 3px solid var(--primary-color); + padding: 6px; + position: relative; +} + +html.writer-html4 .rst-content dl:not(.docutils)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt a { + color: var(--tertiary-color); + text-decoration: underline; +} + +html.writer-html4 .rst-content dl:not(.docutils)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt a:hover, +html.writer-html4 .rst-content dl:not(.docutils)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt a:focus, +html.writer-html4 .rst-content dl:not(.docutils)>dt, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt a:active { + color: var(--tertiary-color); + text-decoration: none; +} + +/* Api doc + * Newlines (\a) and spaces (\20) before each parameter + * https://github.com/sphinx-doc/sphinx/issues/1514 + */ + +/* Newlines (\a) and spaces (\20) before each parameter */ +.sig-param::before { + content: "\a\20\20\20\20\20\20\20\20\20\20\20\20\20\20\20\20"; + white-space: pre; +} + +/* Newline after the last parameter (so the closing bracket is on a new line) */ +dt em.sig-param:last-of-type::after { + content: "\a"; + white-space: pre; +} + +/* To have blue background of width of the block (instead of width of content) */ +dl.class > dt:first-of-type { + display: block !important; +} + + /* + * Admonitions + */ + +/* Important admonition */ +.rst-content .important .admonition-title { + background: var(--secondary-color); +} + +.rst-content .important { + background: var(--secondary-color-light); +} + +/* Hint admonition */ +.rst-content .hint .admonition-title { + background: var(--tertiary-color); +} + +.rst-content .hint { + background: var(--tertiary-color-light); +} + +/* Note admonition */ + +.rst-content .note .admonition-title { + background: var(--tertiary-color); + color: #FFFFFF; +} + +.rst-content .note { + background: var(--tertiary-color-light); } diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 000000000..5883f91f4 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,260 @@ +{# TEMPLATE VAR SETTINGS #} +{%- set url_root = pathto('', 1) %} +{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} +{%- if not embedded and docstitle %} + {%- set titlesuffix = " — "|safe + docstitle|e %} +{%- else %} + {%- set titlesuffix = "" %} +{%- endif %} +{%- set lang_attr = 'en' if language == None else (language | replace('_', '-')) %} +{%- set sphinx_writer = 'writer-html5' if html5_doctype else 'writer-html4' -%} + +{# Build sphinx_version_info tuple from sphinx_version string in pure Jinja #} +{%- set (_ver_major, _ver_minor, _ver_bugfix) = sphinx_version.split('.') | map('int') -%} +{%- set sphinx_version_info = (_ver_major, _ver_minor, _ver_bugfix) -%} + + + + + + {{- metatags }} + + {%- block htmltitle %} + {{ title|striptags|e }}{{ titlesuffix }} + {%- endblock -%} + + {#- CSS #} + {%- if sphinx_version_info < (4, 0) -%} + + + {%- endif %} + {%- for css in css_files %} + {%- if css|attr("rel") %} + + {%- else %} + + {%- endif %} + {%- endfor %} + + {%- for cssfile in extra_css_files %} + + {%- endfor -%} + + {#- FAVICON #} + {%- if favicon %} + {%- if sphinx_version_info < (4, 0) -%} + + {%- else %} + + {%- endif %} + {%- endif -%} + + {#- CANONICAL URL (deprecated) #} + {%- if theme_canonical_url and not pageurl %} + + {%- endif -%} + + {#- CANONICAL URL #} + {%- if pageurl %} + + {%- endif -%} + + {#- JAVASCRIPTS #} + {%- block scripts %} + + {%- if not embedded %} + {# XXX Sphinx 1.8.0 made this an external js-file, quick fix until we refactor the template to inherert more blocks directly from sphinx #} + {%- if sphinx_version_info >= (1, 8) -%} + {%- if sphinx_version_info < (4, 0) -%} + + {%- endif -%} + {%- for scriptfile in script_files %} + {{ js_tag(scriptfile) }} + {%- endfor %} + {%- else %} + + {%- for scriptfile in script_files %} + + {%- endfor %} + {%- endif %} + + + {#- OPENSEARCH #} + {%- if use_opensearch %} + + {%- endif %} + {%- endif %} + {%- endblock %} + + {%- block linktags %} + {%- if hasdoc('about') %} + + {%- endif %} + {%- if hasdoc('genindex') %} + + {%- endif %} + {%- if hasdoc('search') %} + + {%- endif %} + {%- if hasdoc('copyright') %} + + {%- endif %} + {%- if next %} + + {%- endif %} + {%- if prev %} + + {%- endif %} + {%- endblock %} + {%- block extrahead %} {% endblock %} + + + +

+ {%- block extrabody %} {% endblock %} +
+ {% include "versions.html" -%} + + + + {#- Do not conflict with RTD insertion of analytics script #} + {%- if not READTHEDOCS %} + {%- if theme_analytics_id %} + + + + + {%- endif %} + {%- endif %} + + {%- block footer %} {% endblock %} + + + diff --git a/docs/conf.py b/docs/conf.py index 8f206847e..7fb681920 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,6 +36,7 @@ extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.napoleon", + "sphinx_copybutton", ] myst_enable_extensions = [ @@ -68,8 +69,10 @@ html_style = "css/zama.css" html_logo = "logo-black.png" html_theme_options = { "logo_only": False, - "display_version": True, } +html_last_updated_fmt = None # '%b %d, %Y' +html_show_copyright = True +html_show_sphinx = False pygments_style = "zenburn" # Add any paths that contain custom static files (such as style sheets) here, diff --git a/poetry.lock b/poetry.lock index fa591a9d8..889abbd1b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1351,6 +1351,21 @@ docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] +[[package]] +name = "sphinx-copybutton" +version = "0.4.0" +description = "Add a copy button to each of your code cells." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +sphinx = ">=1.8" + +[package.extras] +code_style = ["pre-commit (==2.12.1)"] +rtd = ["sphinx", "ipython", "sphinx-book-theme"] + [[package]] name = "sphinx-rtd-theme" version = "1.0.0" @@ -1585,7 +1600,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "945286da1d6371aafb20c064bfceaf978b9c40d8f19659c5be0ea0c599afd50a" +content-hash = "5d1a54ac0a00d494678a66161c3914bdadd046bd4a5b5e886cf336f966b09da7" [metadata.files] alabaster = [ @@ -2518,6 +2533,10 @@ sphinx = [ {file = "Sphinx-4.2.0-py3-none-any.whl", hash = "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"}, {file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"}, ] +sphinx-copybutton = [ + {file = "sphinx-copybutton-0.4.0.tar.gz", hash = "sha256:8daed13a87afd5013c3a9af3575cc4d5bec052075ccd3db243f895c07a689386"}, + {file = "sphinx_copybutton-0.4.0-py3-none-any.whl", hash = "sha256:4340d33c169dac6dd82dce2c83333412aa786a42dd01a81a8decac3b130dc8b0"}, +] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, diff --git a/pyproject.toml b/pyproject.toml index 775ee7c4b..1ce6fe384 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ tqdm = "^4.62.2" psutil = "^5.8.0" py-cpuinfo = "^8.0.0" python-dotenv = "^0.19.0" +sphinx-copybutton = "^0.4.0" [build-system] requires = ["poetry-core>=1.0.0"] From e525da2b0009c174e4633094e68eb3fac03a6f1d Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Fri, 24 Sep 2021 14:30:23 +0200 Subject: [PATCH 0315/1104] docs: small fix on header --- docs/_static/css/zama.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index 693cf8fd3..bd74c1373 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -21,6 +21,7 @@ right: 0; z-index: 201; height: 43px; + max-width: 1100px; background: black; color: white; display: flex; @@ -100,6 +101,13 @@ input[type=color], input[type=date], input[type=datetime-local], input[type=date padding-top: 43px; } +.wy-body-for-nav { + background: rgba(0,0,0,.05); +} +.wy-nav-content-wrap { + background: none; +} + .wy-nav-side { top: 43px; } From 270253975eb1cf126af4f26aa2102dab928a06c7 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 24 Sep 2021 14:24:53 +0200 Subject: [PATCH 0316/1104] feat: let's release the Concrete framework v0.1.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ce6fe384..bd5308378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.1.0rc3" +version = "0.1.0" description = "Concrete Framework" authors = ["Zama "] packages = [ From 55bcc576dca021c0248778ecead340230c04b5c1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 24 Sep 2021 15:35:31 +0200 Subject: [PATCH 0317/1104] chore: bump version to 0.1.1rc1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bd5308378..87badaad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.1.0" +version = "0.1.1rc1" description = "Concrete Framework" authors = ["Zama "] packages = [ From f6af0b07421b5b5352df65456f6e44bc72e76739 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 24 Sep 2021 16:32:46 +0200 Subject: [PATCH 0318/1104] fix: the URL link needs a final slash. --- .github/ISSUE_TEMPLATE/release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index aeaa99580..daa36d476 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -21,7 +21,7 @@ Then: This is the release markdown template you should copy and update: ``` **Docker Image:** ghcr.io/zama-ai/concretefhe:vX.Y.Z -**Documentation:** https://docs.zama.ai/concrete +**Documentation:** https://docs.zama.ai/concrete/ ``` To continue the release cycle: From 5ff102fcf6e59b6446e28b69fd598cc310c2b22e Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 24 Sep 2021 17:29:33 +0200 Subject: [PATCH 0319/1104] fix: develop has a single p. --- docs/README.md | 2 +- docs/dev/howto/DOCUMENTING.md | 2 +- docs/index.rst | 4 ++-- docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3cdf1de02..f32164fce 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,7 @@ In the first version of Concrete, there is a single frontend, called homomorphic Basically, we have divided our documentation into several parts: - one about basic elements, notably description of the installation, that you are currently reading - one dedicated to _users_ of **Concrete**, with tutorials, how-to's and deeper explanations -- and finally, one dedicated to _developpers_ of **Concrete**, who could be internal or external contributors to the framework +- and finally, one dedicated to _developers_ of **Concrete**, who could be internal or external contributors to the framework ## A work in progress diff --git a/docs/dev/howto/DOCUMENTING.md b/docs/dev/howto/DOCUMENTING.md index 1c73f7df1..cd955691a 100644 --- a/docs/dev/howto/DOCUMENTING.md +++ b/docs/dev/howto/DOCUMENTING.md @@ -11,7 +11,7 @@ make docs Remark that this needs to be done in docker. -The documentation contains both files written by hand by developpers (the .md files) and files automatically created by parsing the source files. +The documentation contains both files written by hand by developers (the .md files) and files automatically created by parsing the source files. ### Opening doc diff --git a/docs/index.rst b/docs/index.rst index e75fb5d81..7ddeea81a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,7 +52,7 @@ Concrete Framework's documentation .. toctree:: :maxdepth: 2 - :caption: Developper - How To + :caption: Developer - How To dev/howto/PROJECT_SETUP.md dev/howto/DOCKER.md @@ -62,7 +62,7 @@ Concrete Framework's documentation .. toctree:: :maxdepth: 2 - :caption: Developper - Explanation + :caption: Developer - Explanation dev/explanation/COMPILATION.md dev/explanation/TERMINOLOGY_AND_STRUCTURE.md diff --git a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md index b87f1c7cb..96a8e8ba4 100644 --- a/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md +++ b/docs/user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md @@ -31,7 +31,7 @@ Once you're sure it is a bug, it would be nice to try to: ## Asking the community -We have created a Slack channel (TODO: LINK TO BE ADDED), such that you can directly ask the developpers and community about your issue. +We have created a Slack channel (TODO: LINK TO BE ADDED), such that you can directly ask the developers and community about your issue. Hopefully, it is just a misunderstanding or a small mistake on your side, that one can help you fix easily. And, the good point with your feedback is that, once we have heard the problem or misunderstanding, we can make the documentation even clearer (such as, completing the FAQ). From 134f5f452c26c565e3b7a84b253bad1671f0f808 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 24 Sep 2021 18:58:00 +0200 Subject: [PATCH 0320/1104] fix: fix url of docs. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5beaf9126..6aeecb9b2 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Information about how to use Docker for development are available in [DOCKER.md] ### Documenting -Some information about how to build the documentation of `concretefhe` are available in [DOCUMENTING.md](docs/dev/howto/DOCUMENTING.md). Notably, our documentation is pushed to [https://hdk.zama.ai](https://hdk.zama.ai). +Some information about how to build the documentation of `concretefhe` are available in [DOCUMENTING.md](docs/dev/howto/DOCUMENTING.md). Notably, our documentation is pushed to [https://docs.zama.ai/concretefhe/](https://docs.zama.ai/concretefhe/). ### Developing From 2b5f152f23bde775a41ae4b2fc3203daf2a308e6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 27 Sep 2021 09:53:00 +0200 Subject: [PATCH 0321/1104] fix(tests): disable warnings for notebook tests - a warning in a package unrelated to the project made pytest fail - run notebook tests without warnings as sources are already tested with warnings treated as errors --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index db4c2591d..39c416658 100644 --- a/Makefile +++ b/Makefile @@ -178,8 +178,10 @@ finalize_nb: poetry run python ./script/nbmake_utils/notebook_finalize.py $(NOTEBOOKS_DIR) .PHONY: finalize_nb +# A warning in a package unrelated to the project made pytest fail with notebooks +# Run notebook tests without warnings as sources are already tested with warnings treated as errors pytest_nb: - poetry run pytest --nbmake $(NOTEBOOKS_DIR)/*.ipynb + poetry run pytest -Wignore --nbmake $(NOTEBOOKS_DIR)/*.ipynb .PHONY: pytest_nb benchmark: From 9a29f4613c590873930638ddfc46e1aef4cc8bcc Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 27 Sep 2021 16:18:53 +0200 Subject: [PATCH 0322/1104] chore: bump version to 0.2.0rc1 - main is now the current version dev branch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87badaad4..bde89303c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.1.1rc1" +version = "0.2.0rc1" description = "Concrete Framework" authors = ["Zama "] packages = [ From 3408bba1ede0ddf414004e05911ade97fd31d31b Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 27 Sep 2021 13:28:14 +0300 Subject: [PATCH 0323/1104] fix: properly ignore measurement scripts during benchmarks --- script/progress_tracker_utils/measure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index fe6fad3a6..e1aa822ca 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -258,7 +258,7 @@ def main(): scripts = list(base.glob("*.py")) # Process each script under the base directory - for script in filter(lambda script: not str(scripts[0]).endswith("measure.py"), scripts): + for script in filter(lambda script: not str(script).endswith("measure.py"), scripts): # Read the script line by line with open(script, "r", encoding="utf-8") as f: lines = f.readlines() From ad95aba0536f85880f15767c72f92a0ae6c80fbc Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 27 Sep 2021 13:28:35 +0300 Subject: [PATCH 0324/1104] chore: run extract_machine_info.py before running the benchmark target, fix missing MACHINE_NAME variable exception --- Makefile | 3 +++ .../benchmark_and_publish_findings_in_docker.sh | 2 +- script/progress_tracker_utils/extract_machine_info.py | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 39c416658..6569e25c7 100644 --- a/Makefile +++ b/Makefile @@ -144,6 +144,8 @@ docker_bas: docker_build_and_start docker_publish_measurements: docker_build mkdir -p .benchmarks + @# Poetry is not installed on the benchmark servers + @# Thus, we ran `extract_machine_info.py` script using native python python script/progress_tracker_utils/extract_machine_info.py docker run --rm --volume /"$$(pwd)":/src $(DEV_DOCKER_IMG) \ /bin/bash ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -185,6 +187,7 @@ pytest_nb: .PHONY: pytest_nb benchmark: + poetry run python script/progress_tracker_utils/extract_machine_info.py poetry run python script/progress_tracker_utils/measure.py benchmarks .PHONY: benchmark diff --git a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh index 52b73f02c..99c8c4dae 100755 --- a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh +++ b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -17,7 +17,7 @@ export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so initial_log=logs/$(date -u --iso-8601=seconds).log mkdir -p logs -make -s benchmark > "$initial_log" +poetry run python script/progress_tracker_utils/measure.py benchmarks > "$initial_log" final_log=logs/$(date -u --iso-8601=seconds).log diff --git a/script/progress_tracker_utils/extract_machine_info.py b/script/progress_tracker_utils/extract_machine_info.py index c8bd4e5df..91eea2434 100644 --- a/script/progress_tracker_utils/extract_machine_info.py +++ b/script/progress_tracker_utils/extract_machine_info.py @@ -30,9 +30,10 @@ def main(): os_value = f"{platform.system()} {platform.release()}" properties.append(["OS", os_value]) - name = os.getenv("MACHINE_NAME").strip() + name = os.getenv("MACHINE_NAME") if name is None: - name = platform.node().strip() + name = platform.node() + name = name.strip() id_ = name.lower() id_ = id_.replace(" ", "-") From 3a13c0b8941d8a2f05b9db5e7d2a55558165e723 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 27 Sep 2021 18:19:01 +0200 Subject: [PATCH 0325/1104] test: add x - 24 in benchmark, doesn't compile refs #471 --- benchmarks/x_minus_24.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 benchmarks/x_minus_24.py diff --git a/benchmarks/x_minus_24.py b/benchmarks/x_minus_24.py new file mode 100644 index 000000000..16e711f8a --- /dev/null +++ b/benchmarks/x_minus_24.py @@ -0,0 +1,43 @@ +# Target: x - 24 + +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x - 24 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(6)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** 6)], + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(40, 40 + 2 ** 3 - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() From b94c0e7959d9f5e1e8e30b4f710f17fa81b705d4 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 27 Sep 2021 18:19:15 +0200 Subject: [PATCH 0326/1104] test: add x - y in benchmark, doesn't compile refs #471 --- benchmarks/x_minus_y.py | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 benchmarks/x_minus_y.py diff --git a/benchmarks/x_minus_y.py b/benchmarks/x_minus_y.py new file mode 100644 index 000000000..ca04b3117 --- /dev/null +++ b/benchmarks/x_minus_y.py @@ -0,0 +1,44 @@ +# Target: x - y + +import itertools +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x - y + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(2)) + + inputset = itertools.product(range(4, 8), range(0, 4)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(2 ** 2, 2 ** 3 - 1) + sample_y = random.randint(0, 2 ** 2 - 1) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() From 312a30a4b5e6ef51a0e642323776248097047a8d Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 27 Sep 2021 18:20:14 +0200 Subject: [PATCH 0327/1104] test: add 124 - x in benchmark, compiles refs #471 --- benchmarks/124_minus_x.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 benchmarks/124_minus_x.py diff --git a/benchmarks/124_minus_x.py b/benchmarks/124_minus_x.py new file mode 100644 index 000000000..8a574e254 --- /dev/null +++ b/benchmarks/124_minus_x.py @@ -0,0 +1,43 @@ +# Target: 124 - x + +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return 124 - x + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** 3)], + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** 3 - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() From b3ffb45a1d68c7b5ac723c35071b0286c6befee6 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 27 Sep 2021 18:26:06 +0200 Subject: [PATCH 0328/1104] test: add x * 7 in benchmark, compiles refs #471 --- benchmarks/x_times_7.py | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 benchmarks/x_times_7.py diff --git a/benchmarks/x_times_7.py b/benchmarks/x_times_7.py new file mode 100644 index 000000000..1faa58112 --- /dev/null +++ b/benchmarks/x_times_7.py @@ -0,0 +1,43 @@ +# Target: x * 7 + +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x * 7 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(4)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** 4)], + ) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** 4 - 1) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() From 95c6bcc6a37107c9f4fed70b6034e3d201e320c5 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 27 Sep 2021 18:31:54 +0200 Subject: [PATCH 0329/1104] test: add x * y in benchmark, does not compile refs #471 --- benchmarks/x_times_y.py | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 benchmarks/x_times_y.py diff --git a/benchmarks/x_times_y.py b/benchmarks/x_times_y.py new file mode 100644 index 000000000..b29c9440c --- /dev/null +++ b/benchmarks/x_times_y.py @@ -0,0 +1,44 @@ +# Target: x * y + +import itertools +import random + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x * y + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(2)) + + inputset = itertools.product(range(4, 8), range(0, 4)) + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(2 ** 2, 2 ** 3 - 1) + sample_y = random.randint(0, 2 ** 2 - 1) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() From 13ccf7a04656d93a845943ddcf7d9f2994cae332 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 27 Sep 2021 18:00:49 +0200 Subject: [PATCH 0330/1104] chore(build): add commit format checks - refactor the way conformance is done - run all conformance checks and aggregate the results in a single step --- .github/workflows/continuous-integration.yaml | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index dc84d3074..1fea9372e 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -146,6 +146,9 @@ jobs: credentials: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_TOKEN }} + defaults: + run: + shell: '/usr/bin/bash -e {0}' strategy: matrix: python-version: [3.8] @@ -177,21 +180,66 @@ jobs: ${{ runner.os }}-build- ${{ runner.os }}- - name: Install dependencies + id: install-deps run: | python -m pip install --upgrade pip python -m pip install poetry make setup_env - - name: Conformance and Docs build - id: conformance - if: ${{ success() && !cancelled() }} + - name: Check commits first line format + id: ccfl + if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} + uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 + with: + pattern: '^((feat|fix|chore|refactor|style|test|docs)(\(\w+\))?\:) .+$' + flags: 'gs' + error: "Your first line has to contain a commit type and scope like \"feat(my_feature): msg\".\ + Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\(\\w+\\))?\\:)'" + excludeDescription: 'true' # optional: this excludes the description body of a pull request + excludeTitle: 'true' # optional: this excludes the title of a pull request + checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request + accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true + - name: Check commits line length + id: ccll + if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} + uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 + with: + pattern: '(^.{0,74}$\r?\n?){0,20}' + flags: 'gm' + error: 'The maximum line length of 74 characters is exceeded.' + excludeDescription: 'true' # optional: this excludes the description body of a pull request + excludeTitle: 'true' # optional: this excludes the title of a pull request + checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request + accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true + - name: Source code Conformance + id: cs + if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} env: # TODO: remove this when JIT doesn't need this # Required to be sure that docs reads all files with MLIR imports properly LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so # pcc launches an internal target with proper flags - # docs is run here too as it can fail and we catch errors during the build run: | - make --keep-going pcc docs + make pcc + - name: Build docs + id: cbd + if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} + env: + # TODO: remove this when JIT doesn't need this + # Required to be sure that docs reads all files with MLIR imports properly + LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so + run: | + make docs + - name: Conformance status + id: conformance + if: ${{ always() && !cancelled() }} + env: + CONFORMANCE_STATUS: ${{ steps.ccfl.outcome == 'success' && steps.ccll.outcome == 'success' && steps.cs.outcome == 'success' && steps.cbd.outcome == 'success' }} + run: | + if [[ "${CONFORMANCE_STATUS}" != "true" ]]; then + echo "Conformance failed, check logs" + exit 1 + fi + - name: Archive docs artifacts if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 From e6bdd52f4df6ed1f787bd0f0e04707469bfb67c0 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 10:56:48 +0300 Subject: [PATCH 0331/1104] fix: remove additional newline on exported graph descriptions --- concrete/common/compilation/artifacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index 88746a10d..b1b48b90d 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -183,7 +183,7 @@ class CompilationArtifacts: for index, (name, representation) in enumerate(textual_representations): identifier = CompilationArtifacts._identifier(index, name) with open(output_directory.joinpath(f"{identifier}.txt"), "w", encoding="utf-8") as f: - f.write(f"{representation}\n") + f.write(f"{representation}") if self.bounds_of_the_final_operation_graph is not None: custom_assert(self.final_operation_graph is not None) From c0fa3027082f02f9e242a5f89c5d024c88ed70dc Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 10:58:26 +0300 Subject: [PATCH 0332/1104] fix: remove double assignment on mlir converter --- concrete/common/mlir/converters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index b9bc1452a..1eedf0ee0 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -61,7 +61,7 @@ def _add_eint_int(node, preds, ir_to_mlir_node, ctx): def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): """Convert an addition intermediate node with (eint, int).""" lhs_node, rhs_node = preds - lhs, rhs = lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] + lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.AddEintOp( hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), lhs, From 77690fed84e7f9c488f92ae27aee09db7074d92e Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 11:32:22 +0300 Subject: [PATCH 0333/1104] fix: generalize error message for unsupported functions --- concrete/numpy/compile.py | 2 +- tests/numpy/test_compile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index a00cbc030..0d7bb559d 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -157,7 +157,7 @@ def _compile_numpy_function_into_op_graph_internal( # Make sure the graph can be lowered to MLIR if not is_graph_values_compatible_with_mlir(op_graph): - raise TypeError("signed integers aren't supported for MLIR lowering") + raise RuntimeError("function you are trying to compile isn't supported for MLIR lowering") # Update bit_width for MLIR update_bit_width_for_mlir(op_graph) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 1ef3cd0c3..5f30b9838 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -251,7 +251,7 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): arg_name: EncryptedScalar(Integer(64, True)) for arg_name in list_of_arg_names } - with pytest.raises(TypeError, match=r"signed integers aren't supported for MLIR lowering"): + with pytest.raises(RuntimeError, match=".*isn't supported for MLIR lowering.*"): compile_numpy_function_into_op_graph( function, function_parameters, From 36d732b0ae24487d4640f6b5a68ced0820c5d06e Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 11:32:53 +0300 Subject: [PATCH 0334/1104] refactor: rename 'data_type' field of 'BaseValue' to 'dtype' --- .../bounds_measurement/inputset_eval.py | 2 +- concrete/common/common_helpers.py | 4 ++-- concrete/common/data_types/dtypes_helpers.py | 20 +++++++++---------- concrete/common/mlir/converters.py | 14 ++++++------- concrete/common/mlir/mlir_converter.py | 10 ++++------ concrete/common/mlir/utils.py | 12 +++++------ concrete/common/operator_graph.py | 10 +++++----- concrete/common/optimization/topological.py | 8 ++++---- .../common/representation/intermediate.py | 10 +++++----- concrete/common/values/base.py | 8 ++++---- concrete/common/values/scalars.py | 14 ++++++------- concrete/common/values/tensors.py | 18 ++++++++--------- concrete/numpy/compile.py | 4 ++-- concrete/numpy/np_dtypes_helpers.py | 4 ++-- concrete/numpy/tracing.py | 2 +- .../bounds_measurement/test_inputset_eval.py | 2 +- tests/common/data_types/test_values.py | 8 ++++---- tests/common/test_common_helpers.py | 8 ++++---- tests/numpy/test_tracing.py | 2 +- 19 files changed, 79 insertions(+), 81 deletions(-) diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index 4be083f5f..52cd6c21e 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -42,7 +42,7 @@ def _check_input_coherency( base_value = base_value_class(is_encrypted=parameter_base_value.is_encrypted) if base_value.shape != parameter_base_value.shape or not is_data_type_compatible_with( - base_value.data_type, parameter_base_value.data_type + base_value.dtype, parameter_base_value.dtype ): warnings.append( f"expected {str(parameter_base_value)} " diff --git a/concrete/common/common_helpers.py b/concrete/common/common_helpers.py index 7aa55eda4..53b3380c1 100644 --- a/concrete/common/common_helpers.py +++ b/concrete/common/common_helpers.py @@ -31,8 +31,8 @@ def ir_nodes_has_integer_input_and_output(node: IntermediateNode) -> bool: Returns: bool: True if all input and output values hold Integers """ - return all(isinstance(x.data_type, Integer) for x in node.inputs) and all( - isinstance(x.data_type, Integer) for x in node.outputs + return all(isinstance(x.dtype, Integer) for x in node.inputs) and all( + isinstance(x.dtype, Integer) for x in node.outputs ) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 8f5ed10a9..2a6f17dd0 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -47,7 +47,7 @@ def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> boo """ return ( value_is_encrypted_scalar_integer(value_to_check) - and not cast(Integer, value_to_check.data_type).is_signed + and not cast(Integer, value_to_check.dtype).is_signed ) @@ -73,7 +73,7 @@ def value_is_scalar_integer(value_to_check: BaseValue) -> bool: bool: True if the passed value_to_check is a ScalarValue of type Integer """ return isinstance(value_to_check, ScalarValue) and isinstance( - value_to_check.data_type, INTEGER_TYPES + value_to_check.dtype, INTEGER_TYPES ) @@ -101,7 +101,7 @@ def value_is_encrypted_tensor_unsigned_integer(value_to_check: BaseValue) -> boo """ return ( value_is_encrypted_tensor_integer(value_to_check) - and not cast(Integer, value_to_check.data_type).is_signed + and not cast(Integer, value_to_check.dtype).is_signed ) @@ -127,7 +127,7 @@ def value_is_tensor_integer(value_to_check: BaseValue) -> bool: bool: True if the passed value_to_check is a TensorValue of type Integer """ return isinstance(value_to_check, TensorValue) and isinstance( - value_to_check.data_type, INTEGER_TYPES + value_to_check.dtype, INTEGER_TYPES ) @@ -216,7 +216,7 @@ def mix_scalar_values_determine_holding_dtype( isinstance(value2, ScalarValue), f"Unsupported value2: {value2}, expected ScalarValue" ) - holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) + holding_type = find_type_to_hold_both_lossy(value1.dtype, value2.dtype) mixed_value: ScalarValue if value1.is_encrypted or value2.is_encrypted: @@ -261,13 +261,13 @@ def mix_tensor_values_determine_holding_dtype( ), ) - holding_type = find_type_to_hold_both_lossy(value1.data_type, value2.data_type) + holding_type = find_type_to_hold_both_lossy(value1.dtype, value2.dtype) shape = value1.shape if value1.is_encrypted or value2.is_encrypted: - mixed_value = EncryptedTensor(data_type=holding_type, shape=shape) + mixed_value = EncryptedTensor(dtype=holding_type, shape=shape) else: - mixed_value = ClearTensor(data_type=holding_type, shape=shape) + mixed_value = ClearTensor(dtype=holding_type, shape=shape) return mixed_value @@ -362,10 +362,10 @@ def get_base_value_for_python_constant_data( assert len(constant_data) > 0 constant_shape = (len(constant_data),) constant_data_type = get_base_data_type_for_python_constant_data(constant_data) - return partial(TensorValue, data_type=constant_data_type, shape=constant_shape) + return partial(TensorValue, dtype=constant_data_type, shape=constant_shape) constant_data_type = get_base_data_type_for_python_constant_data(constant_data) - return partial(ScalarValue, data_type=constant_data_type) + return partial(ScalarValue, dtype=constant_data_type) def get_type_constructor_for_python_constant_data(constant_data: Union[int, float]): diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 1eedf0ee0..9cb12213a 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -52,7 +52,7 @@ def _add_eint_int(node, preds, ir_to_mlir_node, ctx): lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.AddEintIntOp( - hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].dtype.bit_width), lhs, rhs, ).result @@ -63,7 +63,7 @@ def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.AddEintOp( - hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].dtype.bit_width), lhs, rhs, ).result @@ -87,7 +87,7 @@ def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.SubIntEintOp( - hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].dtype.bit_width), lhs, rhs, ).result @@ -116,7 +116,7 @@ def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): lhs_node, rhs_node = preds lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.MulEintIntOp( - hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].dtype.bit_width), lhs, rhs, ).result @@ -126,7 +126,7 @@ def constant(node, _, __, ctx): """Convert a constant inputs.""" if not value_is_clear_scalar_integer(node.outputs[0]): raise TypeError("Don't support non-integer constants") - dtype = cast(Integer, node.outputs[0].data_type) + dtype = cast(Integer, node.outputs[0].dtype) if dtype.is_signed: raise TypeError("Don't support signed constant integer") int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) @@ -145,7 +145,7 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): x_node = preds[0] x = ir_to_mlir_node[x_node] table = node.get_table() - out_dtype = cast(Integer, node.outputs[0].data_type) + out_dtype = cast(Integer, node.outputs[0].dtype) # Create table dense_elem = DenseElementsAttr.get(np.array(table, dtype=np.uint64), context=ctx) tensor_lut = std_dialect.ConstantOp( @@ -182,7 +182,7 @@ def dot(node, preds, ir_to_mlir_node, ctx): lhs_node, rhs_node = rhs_node, lhs_node lhs, rhs = ir_to_mlir_node[lhs_node], ir_to_mlir_node[rhs_node] return hlfhe.Dot( - hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].data_type.bit_width), + hlfhe.EncryptedIntegerType.get(ctx, node.outputs[0].dtype.bit_width), lhs, rhs, ).result diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index aec6b7906..177c538dd 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -101,16 +101,14 @@ class MLIRConverter: corresponding MLIR type """ if value_is_encrypted_scalar_unsigned_integer(value): - return self._get_scalar_integer_type( - cast(Integer, value.data_type).bit_width, True, False - ) + return self._get_scalar_integer_type(cast(Integer, value.dtype).bit_width, True, False) if value_is_clear_scalar_integer(value): - dtype = cast(Integer, value.data_type) + dtype = cast(Integer, value.dtype) return self._get_scalar_integer_type( dtype.bit_width, is_encrypted=False, is_signed=dtype.is_signed ) if value_is_encrypted_tensor_unsigned_integer(value): - dtype = cast(Integer, value.data_type) + dtype = cast(Integer, value.dtype) return self._get_tensor_type( dtype.bit_width, is_encrypted=True, @@ -118,7 +116,7 @@ class MLIRConverter: shape=cast(values.TensorValue, value).shape, ) if value_is_clear_tensor_integer(value): - dtype = cast(Integer, value.data_type) + dtype = cast(Integer, value.dtype) return self._get_tensor_type( dtype.bit_width, is_encrypted=False, diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 3b90d349e..28280a4ad 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -28,7 +28,7 @@ def is_graph_values_compatible_with_mlir(op_graph: OPGraph) -> bool: """ return all( all( - value_is_scalar_integer(out) and not cast(Integer, out.data_type).is_signed + value_is_scalar_integer(out) and not cast(Integer, out.dtype).is_signed for out in out_node.outputs ) for out_node in op_graph.output_nodes.values() @@ -45,11 +45,11 @@ def _set_all_bit_width(op_graph: OPGraph, p: int): for node in op_graph.graph.nodes: for value in node.outputs + node.inputs: if value_is_clear_scalar_integer(value) or value_is_clear_tensor_integer(value): - value.data_type.bit_width = p + 1 + value.dtype.bit_width = p + 1 elif value_is_encrypted_scalar_integer(value) or value_is_encrypted_tensor_integer( value ): - value.data_type.bit_width = p + value.dtype.bit_width = p def update_bit_width_for_mlir(op_graph: OPGraph): @@ -63,7 +63,7 @@ def update_bit_width_for_mlir(op_graph: OPGraph): for node in op_graph.graph.nodes: for value_out in node.outputs: if value_is_clear_scalar_integer(value_out) or value_is_clear_tensor_integer(value_out): - current_node_out_bit_width = value_out.data_type.bit_width - 1 + current_node_out_bit_width = value_out.dtype.bit_width - 1 else: assert_true( @@ -71,7 +71,7 @@ def update_bit_width_for_mlir(op_graph: OPGraph): or value_is_encrypted_tensor_integer(value_out) ) - current_node_out_bit_width = value_out.data_type.bit_width + current_node_out_bit_width = value_out.dtype.bit_width max_bit_width = max(max_bit_width, current_node_out_bit_width) @@ -106,7 +106,7 @@ def extend_direct_lookup_tables(op_graph: OPGraph): for node in op_graph.graph.nodes: if isinstance(node, ArbitraryFunction) and node.op_name == "TLU": table = node.op_kwargs["table"] - bit_width = cast(Integer, node.inputs[0].data_type).bit_width + bit_width = cast(Integer, node.inputs[0].dtype).bit_width expected_length = 2 ** bit_width # TODO: remove no cover once the table length workaround is removed diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index 7d8464cb9..ec3e06dc2 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -196,7 +196,7 @@ class OPGraph: if not isinstance(node, Input): for output_value in node.outputs: if isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer): - output_value.data_type = make_integer_to_hold( + output_value.dtype = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) else: @@ -208,8 +208,8 @@ class OPGraph: f"min_bound: {min_data_type}, max_bound: {max_data_type}" ), ) - output_value.data_type = Float(64) - output_value.data_type.underlying_type_constructor = data_type_constructor + output_value.dtype = Float(64) + output_value.dtype.underlying_type_constructor = data_type_constructor else: # Currently variable inputs are only allowed to be integers custom_assert( @@ -220,10 +220,10 @@ class OPGraph: f"max: {max_bound} ({type(max_bound)})" ), ) - node.inputs[0].data_type = make_integer_to_hold( + node.inputs[0].dtype = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) - node.inputs[0].data_type.underlying_type_constructor = data_type_constructor + node.inputs[0].dtype.underlying_type_constructor = data_type_constructor node.outputs[0] = deepcopy(node.inputs[0]) diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index 18a7f75e4..ab62a940a 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -160,7 +160,7 @@ def convert_float_subgraph_to_fused_node( lambda x, float_op_subgraph, terminal_node: float_op_subgraph.evaluate({0: x})[ terminal_node ], - deepcopy(terminal_node.outputs[0].data_type), + deepcopy(terminal_node.outputs[0].dtype), op_kwargs={ "float_op_subgraph": float_op_subgraph, "terminal_node": terminal_node, @@ -197,13 +197,13 @@ def find_float_subgraph_with_unique_terminal_node( def is_float_to_single_int_node(node: IntermediateNode) -> bool: return ( - any(isinstance(input_.data_type, Float) for input_ in node.inputs) + any(isinstance(input_.dtype, Float) for input_ in node.inputs) and len(node.outputs) == 1 - and isinstance(node.outputs[0].data_type, Integer) + and isinstance(node.outputs[0].dtype, Integer) ) def single_int_output_node(node: IntermediateNode) -> bool: - return len(node.outputs) == 1 and isinstance(node.outputs[0].data_type, Integer) + return len(node.outputs) == 1 and isinstance(node.outputs[0].dtype, Integer) float_subgraphs_terminal_nodes = ( node diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index e42fc4b27..4ee420e72 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -242,21 +242,21 @@ class ArbitraryFunction(IntermediateNode): """ # Check the input is an unsigned integer to be able to build a table assert isinstance( - self.inputs[0].data_type, Integer + self.inputs[0].dtype, Integer ), "get_table only works for an unsigned Integer input" assert not self.inputs[ 0 - ].data_type.is_signed, "get_table only works for an unsigned Integer input" + ].dtype.is_signed, "get_table only works for an unsigned Integer input" - type_constructor = self.inputs[0].data_type.underlying_type_constructor + type_constructor = self.inputs[0].dtype.underlying_type_constructor if type_constructor is None: logger.info( f"{self.__class__.__name__} input data type constructor was None, defaulting to int" ) type_constructor = int - min_input_range = self.inputs[0].data_type.min_value() - max_input_range = self.inputs[0].data_type.max_value() + 1 + min_input_range = self.inputs[0].dtype.min_value() + max_input_range = self.inputs[0].dtype.max_value() + 1 table = [ self.evaluate({0: type_constructor(input_value)}) diff --git a/concrete/common/values/base.py b/concrete/common/values/base.py index 39fd770ef..25311f66a 100644 --- a/concrete/common/values/base.py +++ b/concrete/common/values/base.py @@ -9,11 +9,11 @@ from ..data_types.base import BaseDataType class BaseValue(ABC): """Abstract base class to represent any kind of value in a program.""" - data_type: BaseDataType + dtype: BaseDataType _is_encrypted: bool - def __init__(self, data_type: BaseDataType, is_encrypted: bool) -> None: - self.data_type = deepcopy(data_type) + def __init__(self, dtype: BaseDataType, is_encrypted: bool) -> None: + self.dtype = deepcopy(dtype) self._is_encrypted = is_encrypted def __repr__(self) -> str: # pragma: no cover @@ -21,7 +21,7 @@ class BaseValue(ABC): @abstractmethod def __eq__(self, other: object) -> bool: - return isinstance(other, self.__class__) and self.data_type == other.data_type + return isinstance(other, self.__class__) and self.dtype == other.dtype @property def is_encrypted(self) -> bool: diff --git a/concrete/common/values/scalars.py b/concrete/common/values/scalars.py index e1b541e6d..66be4eb26 100644 --- a/concrete/common/values/scalars.py +++ b/concrete/common/values/scalars.py @@ -14,7 +14,7 @@ class ScalarValue(BaseValue): def __str__(self) -> str: # pragma: no cover encrypted_str = "Encrypted" if self._is_encrypted else "Clear" - return f"{encrypted_str}Scalar<{self.data_type!r}>" + return f"{encrypted_str}Scalar<{self.dtype!r}>" @property def shape(self) -> Tuple[int, ...]: @@ -26,28 +26,28 @@ class ScalarValue(BaseValue): return () -def make_clear_scalar(data_type: BaseDataType) -> ScalarValue: +def make_clear_scalar(dtype: BaseDataType) -> ScalarValue: """Create a clear ScalarValue. Args: - data_type (BaseDataType): The data type for the value. + dtype (BaseDataType): The data type for the value. Returns: ScalarValue: The corresponding ScalarValue. """ - return ScalarValue(data_type=data_type, is_encrypted=False) + return ScalarValue(dtype=dtype, is_encrypted=False) -def make_encrypted_scalar(data_type: BaseDataType) -> ScalarValue: +def make_encrypted_scalar(dtype: BaseDataType) -> ScalarValue: """Create an encrypted ScalarValue. Args: - data_type (BaseDataType): The data type for the value. + dtype (BaseDataType): The data type for the value. Returns: ScalarValue: The corresponding ScalarValue. """ - return ScalarValue(data_type=data_type, is_encrypted=True) + return ScalarValue(dtype=dtype, is_encrypted=True) ClearScalar = make_clear_scalar diff --git a/concrete/common/values/tensors.py b/concrete/common/values/tensors.py index dc3d421d0..ded52565e 100644 --- a/concrete/common/values/tensors.py +++ b/concrete/common/values/tensors.py @@ -16,11 +16,11 @@ class TensorValue(BaseValue): def __init__( self, - data_type: BaseDataType, + dtype: BaseDataType, is_encrypted: bool, shape: Optional[Tuple[int, ...]] = None, ) -> None: - super().__init__(data_type, is_encrypted) + super().__init__(dtype, is_encrypted) # Managing tensors as in numpy, no shape or () is treated as a 0-D array of size 1 self._shape = shape if shape is not None else () self._ndim = len(self._shape) @@ -37,7 +37,7 @@ class TensorValue(BaseValue): def __str__(self) -> str: encrypted_str = "Encrypted" if self._is_encrypted else "Clear" - return f"{encrypted_str}Tensor<{str(self.data_type)}, shape={self.shape}>" + return f"{encrypted_str}Tensor<{str(self.dtype)}, shape={self.shape}>" @property def shape(self) -> Tuple[int, ...]: @@ -68,35 +68,35 @@ class TensorValue(BaseValue): def make_clear_tensor( - data_type: BaseDataType, + dtype: BaseDataType, shape: Optional[Tuple[int, ...]] = None, ) -> TensorValue: """Create a clear TensorValue. Args: - data_type (BaseDataType): The data type for the tensor. + dtype (BaseDataType): The data type for the tensor. shape (Optional[Tuple[int, ...]], optional): The tensor shape. Defaults to None. Returns: TensorValue: The corresponding TensorValue. """ - return TensorValue(data_type=data_type, is_encrypted=False, shape=shape) + return TensorValue(dtype=dtype, is_encrypted=False, shape=shape) def make_encrypted_tensor( - data_type: BaseDataType, + dtype: BaseDataType, shape: Optional[Tuple[int, ...]] = None, ) -> TensorValue: """Create an encrypted TensorValue. Args: - data_type (BaseDataType): The data type for the tensor. + dtype (BaseDataType): The data type for the tensor. shape (Optional[Tuple[int, ...]], optional): The tensor shape. Defaults to None. Returns: TensorValue: The corresponding TensorValue. """ - return TensorValue(data_type=data_type, is_encrypted=True, shape=shape) + return TensorValue(dtype=dtype, is_encrypted=True, shape=shape) ClearTensor = make_clear_tensor diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 0d7bb559d..8f7790a90 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -125,9 +125,9 @@ def _compile_numpy_function_into_op_graph_internal( # this loop will determine the number of possible inputs of the function # if a function have a single 3-bit input, for example, `inputset_size_upper_limit` will be 8 for parameter_value in function_parameters.values(): - if isinstance(parameter_value.data_type, Integer): + if isinstance(parameter_value.dtype, Integer): # multiple parameter bit-widths are multiplied as they can be combined into an input - inputset_size_upper_limit *= 2 ** parameter_value.data_type.bit_width + inputset_size_upper_limit *= 2 ** parameter_value.dtype.bit_width # if the upper limit of the inputset size goes above 10, # break the loop as we will require at least 10 inputs in this case diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index cbcdb939d..b4cab219b 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -172,9 +172,9 @@ def get_base_value_for_numpy_or_python_constant_data( base_dtype = get_base_data_type_for_numpy_or_python_constant_data(constant_data) if isinstance(constant_data, numpy.ndarray): - constant_data_value = partial(TensorValue, data_type=base_dtype, shape=constant_data.shape) + constant_data_value = partial(TensorValue, dtype=base_dtype, shape=constant_data.shape) elif isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): - constant_data_value = partial(ScalarValue, data_type=base_dtype) + constant_data_value = partial(ScalarValue, dtype=base_dtype) else: constant_data_value = get_base_value_for_python_constant_data(constant_data) return constant_data_value diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 7376a63f0..6198b7ca7 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -129,7 +129,7 @@ class NPTracer(BaseTracer): @staticmethod def _manage_dtypes(ufunc: Union[numpy.ufunc, Callable], *input_tracers: BaseTracer): output_dtypes = get_numpy_function_output_dtype( - ufunc, [input_tracer.output.data_type for input_tracer in input_tracers] + ufunc, [input_tracer.output.dtype for input_tracer in input_tracers] ) common_output_dtypes = [ convert_numpy_dtype_to_base_data_type(dtype) for dtype in output_dtypes diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index dc3c3d7dc..d622e9ee7 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -296,7 +296,7 @@ def test_eval_op_graph_bounds_on_inputset_multiple_output( op_graph.update_values_with_bounds(node_bounds) for i, output_node in op_graph.output_nodes.items(): - assert expected_output_data_type[i] == output_node.outputs[0].data_type + assert expected_output_data_type[i] == output_node.outputs[0].dtype def test_eval_op_graph_bounds_on_non_conformant_inputset_default(capsys): diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py index d242fc27b..6f18a59ec 100644 --- a/tests/common/data_types/test_values.py +++ b/tests/common/data_types/test_values.py @@ -60,26 +60,26 @@ def test_tensor_value( ): """Test function for TensorValue""" - tensor_value = tensor_constructor(data_type=data_type, shape=shape) + tensor_value = tensor_constructor(dtype=data_type, shape=shape) assert expected_is_encrypted == tensor_value.is_encrypted assert expected_shape == tensor_value.shape assert expected_ndim == tensor_value.ndim assert expected_size == tensor_value.size - assert data_type == tensor_value.data_type + assert data_type == tensor_value.dtype other_tensor = deepcopy(tensor_value) assert other_tensor == tensor_value other_tensor_value = deepcopy(other_tensor) - other_tensor_value.data_type = DummyDtype() + other_tensor_value.dtype = DummyDtype() assert other_tensor_value != tensor_value other_shape = tuple(val + 1 for val in shape) if shape is not None else () other_shape += (2,) - other_tensor_value = tensor_constructor(data_type=data_type, shape=other_shape) + other_tensor_value = tensor_constructor(dtype=data_type, shape=other_shape) assert other_tensor_value.shape != tensor_value.shape assert other_tensor_value.ndim != tensor_value.ndim diff --git a/tests/common/test_common_helpers.py b/tests/common/test_common_helpers.py index deccf46b2..575e118cf 100644 --- a/tests/common/test_common_helpers.py +++ b/tests/common/test_common_helpers.py @@ -46,7 +46,7 @@ def test_check_op_graph_is_integer_program(): assert len(offending_nodes) == 0 op_graph_copy = deepcopy(op_graph) - op_graph_copy.output_nodes[0].outputs[0].data_type = Float64 + op_graph_copy.output_nodes[0].outputs[0].dtype = Float64 offending_nodes = [] assert not check_op_graph_is_integer_program(op_graph_copy) @@ -55,7 +55,7 @@ def test_check_op_graph_is_integer_program(): assert offending_nodes == [op_graph_copy.output_nodes[0]] op_graph_copy = deepcopy(op_graph) - op_graph_copy.input_nodes[0].inputs[0].data_type = Float64 + op_graph_copy.input_nodes[0].inputs[0].dtype = Float64 offending_nodes = [] assert not check_op_graph_is_integer_program(op_graph_copy) @@ -64,8 +64,8 @@ def test_check_op_graph_is_integer_program(): assert offending_nodes == [op_graph_copy.input_nodes[0]] op_graph_copy = deepcopy(op_graph) - op_graph_copy.input_nodes[0].inputs[0].data_type = Float64 - op_graph_copy.input_nodes[1].inputs[0].data_type = Float64 + op_graph_copy.input_nodes[0].inputs[0].dtype = Float64 + op_graph_copy.input_nodes[1].inputs[0].dtype = Float64 offending_nodes = [] assert not check_op_graph_is_integer_program(op_graph_copy) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 76634a779..b855820f1 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -222,7 +222,7 @@ def test_tracing_astype( op_graph = tracing.trace_numpy_function(function_to_trace, {"x": input_value}) output_node = op_graph.output_nodes[0] - assert op_graph_expected_output_type == output_node.outputs[0].data_type + assert op_graph_expected_output_type == output_node.outputs[0].dtype node_results = op_graph.evaluate({0: numpy.array(input_)}) evaluated_output = node_results[output_node] From bc80f0de10e6e145a788e24a9101080f855a1fb4 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 11:43:30 +0300 Subject: [PATCH 0335/1104] refactor: fix new pylint warnings --- concrete/numpy/np_dtypes_helpers.py | 6 +----- tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index b4cab219b..72d3b52f2 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -123,11 +123,7 @@ def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> f"Unsupported constant data of type {type(constant_data)}", ) if isinstance(constant_data, (numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)): - native_type = ( - float - if constant_data.dtype == numpy.float32 or constant_data.dtype == numpy.float64 - else int - ) + native_type = float if (constant_data.dtype in (numpy.float32, numpy.float64)) else int min_value = native_type(constant_data.min()) max_value = native_type(constant_data.max()) diff --git a/tests/conftest.py b/tests/conftest.py index 09d84a55f..2a0d48873 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def _is_equivalent_to_binary_commutative(lhs: IntermediateNode, rhs: object) -> """is_equivalent_to for a binary and commutative operation.""" return ( isinstance(rhs, lhs.__class__) - and (lhs.inputs == rhs.inputs or lhs.inputs == rhs.inputs[::-1]) + and (lhs.inputs in (rhs.inputs, rhs.inputs[::-1])) and lhs.outputs == rhs.outputs ) From 4ef0a13248ef5ec31a0fd27b5ead21eee77ed9bf Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 10:34:17 +0300 Subject: [PATCH 0336/1104] refactor: move measurement scripts to their own directory to avoid pcc errors --- .../extract_machine_info.py | 2 + script/progress_tracker_utils/measure.py | 47 ++++++++++--------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/script/progress_tracker_utils/extract_machine_info.py b/script/progress_tracker_utils/extract_machine_info.py index 91eea2434..f65b6cc78 100644 --- a/script/progress_tracker_utils/extract_machine_info.py +++ b/script/progress_tracker_utils/extract_machine_info.py @@ -45,6 +45,8 @@ def main(): id_ = id_.strip() id_ = urllib.parse.quote_plus(id_) + os.makedirs(".benchmarks", exist_ok=True) + machine = {"id": id_, "name": name, "properties": properties} with open(".benchmarks/machine.json", "w", encoding="utf-8") as f: json.dump(machine, f, indent=2, ensure_ascii=False) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index e1aa822ca..9d8016354 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -97,10 +97,10 @@ def identify_metrics(script, lines, metrics): ) -def create_modified_script(script_without_extension, lines, metrics): +def create_modified_script(script, lines, metrics): """Create a modified version of the script which can be used to perform measurements""" - with open(f"{script_without_extension}.measure.py", "w", encoding="utf-8") as f: + with open(f".benchmarks/scripts/{script}", "w", encoding="utf-8") as f: # Import must-have libraries f.write("import json\n") f.write("import time\n") @@ -160,19 +160,19 @@ def create_modified_script(script_without_extension, lines, metrics): # Dump measurements to a temporary file after the script is executed from start to end f.write("\n") - f.write(f'with open("{script_without_extension}.measurements", "w") as f:\n') + f.write(f'with open(".benchmarks/scripts/{script}.measurements", "w") as f:\n') f.write(" json.dump(_measurements_, f, indent=2)\n") -def perform_measurements(script, script_without_extension, target_id, metrics, samples, result): +def perform_measurements(path, script, target_id, metrics, samples, result): """Run the modified script multiple times and update the result""" # Create a flag to keep track of the working status working = True print() - print(script) - print("-" * len(str(script))) + print(path) + print("-" * len(str(path))) # Run the modified script `samples` times and accumulate measurements measurements = {metric_id: [] for metric_id in metrics.keys()} @@ -180,7 +180,7 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s for i in range(samples): # Create the subprocess process = subprocess.run( - ["python", f"{script_without_extension}.measure.py"], + ["python", f".benchmarks/scripts/{script}"], capture_output=True, check=False, ) @@ -200,14 +200,15 @@ def perform_measurements(script, script_without_extension, target_id, metrics, s for line in stderr.split("\n"): if line.strip() != "": pbar.write(f" {line}") + pbar.write("") pbar.update(samples) break # Read the measurements and delete the temporary file - with open(f"{script_without_extension}.measurements", encoding="utf-8") as f: + with open(f".benchmarks/scripts/{script}.measurements", encoding="utf-8") as f: results = json.load(f) - os.unlink(f"{script_without_extension}.measurements") + os.unlink(f".benchmarks/scripts/{script}.measurements") # Add the `results` of the current run to `measurements` for metric_id in metrics.keys(): @@ -257,10 +258,13 @@ def main(): result = {"machine": machine, "metrics": {}, "targets": {}} scripts = list(base.glob("*.py")) + # Create a directory to store temporary scripts + os.makedirs(".benchmarks/scripts", exist_ok=True) + # Process each script under the base directory - for script in filter(lambda script: not str(script).endswith("measure.py"), scripts): + for path in scripts: # Read the script line by line - with open(script, "r", encoding="utf-8") as f: + with open(path, "r", encoding="utf-8") as f: lines = f.readlines() # Find the first non-empty line @@ -273,8 +277,8 @@ def main(): # Check whether the script is a target or not if not first_line.startswith("# Target:"): print() - print(script) - print("-" * len(str(script))) + print(path) + print("-" * len(str(path))) with tqdm.tqdm(total=samples) as pbar: pbar.write(" Sample 1") @@ -300,24 +304,25 @@ def main(): metrics = {} # Identify metrics of the current script - identify_metrics(script, lines, metrics) + identify_metrics(path, lines, metrics) - # Extract the script name without extension - script_without_extension = os.path.splitext(script)[0] + # Extract the script name + name = os.path.basename(path) # Create another script to hold the modified version of the current script - create_modified_script(script_without_extension, lines, metrics) + create_modified_script(name, lines, metrics) # Perform and save measurements - perform_measurements(script, script_without_extension, target_id, metrics, samples, result) + perform_measurements(path, name, target_id, metrics, samples, result) # Dump the latest results to the output file with open(".benchmarks/findings.json", "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) - # Delete the modified script if the user doesn't care - if not args.keep: - os.unlink(f"{os.path.splitext(script)[0]}.measure.py") + # Delete the modified scripts if the user doesn't care + if not args.keep: + os.unlink(".benchmarks/scripts") + print() From 456df69f0bb9662ad2f5de29d7d175c753a69151 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 29 Sep 2021 12:47:42 +0200 Subject: [PATCH 0337/1104] fix(build): change commit conformance check - action fails if the even is not of a supported type, only check for PR --- .github/workflows/continuous-integration.yaml | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 1fea9372e..029effeba 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -28,6 +28,7 @@ env: LATEST_IMAGE: ghcr.io/zama-ai/concretefhe-env:latest BASE_IMAGE: ghcr.io/zama-ai/concretefhe-env ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + IS_PR: ${{ github.event_name == 'pull_request' }} jobs: build-preflight-docker: @@ -50,8 +51,6 @@ jobs: - name: Get changed files if: ${{ (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) || github.event_name == 'pull_request' }} id: files - env: - IS_PR: ${{ github.event_name == 'pull_request' }} run: | CURRENT_SHA="${{ github.sha }}" if [[ "${IS_PR}" == "true" ]]; then @@ -187,7 +186,7 @@ jobs: make setup_env - name: Check commits first line format id: ccfl - if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} + if: ${{ env.IS_PR && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: pattern: '^((feat|fix|chore|refactor|style|test|docs)(\(\w+\))?\:) .+$' @@ -200,7 +199,7 @@ jobs: accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true - name: Check commits line length id: ccll - if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} + if: ${{ env.IS_PR && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: pattern: '(^.{0,74}$\r?\n?){0,20}' @@ -210,6 +209,17 @@ jobs: excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true + - name: Commit conformance + id: commit-conformance + if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} + env: + CCFL_OK: ${{ (env.IS_PR && steps.ccfl.outcome == 'success') || steps.ccfl.outcome == 'skipped' }} + CCLL_OK: ${{ (env.IS_PR && steps.ccll.outcome == 'success') || steps.ccll.outcome == 'skipped' }} + run: | + if [[ "${CCFL_OK}" != "true" || "${CCLL_OK}" != "true" ]]; then + echo "Issues with commits. First line ok: ${CCFL_OK}. Line length ok: ${CCLL_OK}." + exit 1 + fi - name: Source code Conformance id: cs if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} @@ -233,7 +243,7 @@ jobs: id: conformance if: ${{ always() && !cancelled() }} env: - CONFORMANCE_STATUS: ${{ steps.ccfl.outcome == 'success' && steps.ccll.outcome == 'success' && steps.cs.outcome == 'success' && steps.cbd.outcome == 'success' }} + CONFORMANCE_STATUS: ${{ steps.commit-conformance.outcome == 'success' && steps.cs.outcome == 'success' && steps.cbd.outcome == 'success' }} run: | if [[ "${CONFORMANCE_STATUS}" != "true" ]]; then echo "Conformance failed, check logs" From 10be75f094dbad0762fb27aa937dd6872a193084 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 29 Sep 2021 13:10:08 +0200 Subject: [PATCH 0338/1104] chore(build): manage latest tag push for releases - no push for release candidates - push as latest if version is the highest --- .github/workflows/continuous-integration.yaml | 33 +++++++++--- script/actions_utils/version_comparison.py | 51 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 script/actions_utils/version_comparison.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 029effeba..6b8d889db 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -460,8 +460,27 @@ jobs: - name: Set tag in env run: | GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') - RELEASE_IMG_TAG="${RELEASE_IMAGE_BASE}:${GIT_TAG}" - echo "RELEASE_IMG_TAG=${RELEASE_IMG_TAG}" >> "$GITHUB_ENV" + RELEASE_IMG_GIT_TAG="${RELEASE_IMAGE_BASE}:${GIT_TAG}" + echo "RELEASE_IMG_GIT_TAG=${RELEASE_IMG_GIT_TAG}" >> "$GITHUB_ENV" + RELEASE_IMG_TAGS_TO_PUSH="${RELEASE_IMG_GIT_TAG}" + + EXISTING_TAGS=$(curl \ + -X GET \ + -H "Authorization: Bearer $(echo ${{ secrets.BOT_TOKEN }} | base64)" \ + https://ghcr.io/v2/zama-ai/concretefhe/tags/list | jq -rc '.tags | join(" ")') + + # We want the space separated list of versions to be expanded + # shellcheck disable=SC2086 + REQUIRES_LATEST_TAG=$(python script/actions_utils/version_comparison.py \ + --new-version "${GIT_TAG}" \ + --existing-versions $EXISTING_TAGS) + + if [[ "${REQUIRES_LATEST_TAG}" == "true" ]]; then + RELEASE_IMG_LATEST_TAG="${RELEASE_IMAGE_BASE}:latest" + RELEASE_IMG_TAGS_TO_PUSH="${RELEASE_IMG_TAGS_TO_PUSH},${RELEASE_IMG_LATEST_TAG}" + fi + + echo "RELEASE_IMG_TAGS_TO_PUSH=${RELEASE_IMG_TAGS_TO_PUSH}" >> "$GITHUB_ENV" - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25 @@ -480,21 +499,21 @@ jobs: file: docker/Dockerfile.release load: true push: false - tags: "${{ env.RELEASE_IMG_TAG }}" + tags: "${{ env.RELEASE_IMG_TAGS_TO_PUSH }}" no-cache: true - name: Release image sanity check and push if: ${{ success() && !cancelled() }} run: | - echo "Running sanity check for ${RELEASE_IMG_TAG}" + echo "Running sanity check for ${RELEASE_IMG_GIT_TAG}" docker run --rm --env LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so \ -v "$(pwd)"/docker/release_resources:/data \ - "${RELEASE_IMG_TAG}" /bin/bash -c "python ./sanity_check.py" - docker push "${RELEASE_IMG_TAG}" + "${RELEASE_IMG_GIT_TAG}" /bin/bash -c "python ./sanity_check.py" + docker image push --all-tags "${RELEASE_IMAGE_BASE}" - name: Set notification report id: report if: ${{ always() }} run: | - REPORT="Pushing docker image ${{ env.RELEASE_IMG_TAG }} finished with status \ + REPORT="Pushing docker image ${{ env.RELEASE_IMG_TAGS_TO_PUSH }} finished with status \ ${{ job.status }}." echo "${REPORT}" echo "::set-output name=report::${REPORT}" diff --git a/script/actions_utils/version_comparison.py b/script/actions_utils/version_comparison.py new file mode 100644 index 000000000..f78fd9b35 --- /dev/null +++ b/script/actions_utils/version_comparison.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +"""Helper script for github actions to compare versions""" +import argparse +import re +import sys + + +def main(args): + """Entry point""" + print(args, file=sys.stderr) + semver_matcher = re.compile(r"^(v)?([\d.]+)(rc\d+)?$") + # Keep versions that are not release candidate + all_versions = [ + tuple(map(int, match.group(2).split("."))) + for version in args.existing_versions + if (match := semver_matcher.match(version)) is not None and match.group(3) is None + ] + + nv_match = semver_matcher.match(args.new_version) + new_version = ( + tuple(map(int, nv_match.group(2).split("."))) + if nv_match is not None and nv_match.group(3) is None + else None + ) + + all_versions.append(new_version) + + nv_is_rc = new_version is None + nv_is_latest = not nv_is_rc and max(all_versions) == new_version + print(str(nv_is_latest).lower()) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + "Compare new version to previous versions and determine if it's the latest", + allow_abbrev=False, + ) + + parser.add_argument("--new-version", type=str, required=True, help="The new version to compare") + parser.add_argument( + "--existing-versions", + type=str, + nargs="+", + required=True, + help="The list of existing versions", + ) + + cli_args = parser.parse_args() + + main(cli_args) From f97682bd23c7e888e4fdfca2d7b86c559cfd54ad Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 29 Sep 2021 14:53:55 +0200 Subject: [PATCH 0339/1104] fix(build): convert string to boolean to avoid bug in workflow --- .github/workflows/continuous-integration.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 6b8d889db..7db611ab1 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -186,7 +186,7 @@ jobs: make setup_env - name: Check commits first line format id: ccfl - if: ${{ env.IS_PR && steps.install-deps.outcome == 'success' && !cancelled() }} + if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: pattern: '^((feat|fix|chore|refactor|style|test|docs)(\(\w+\))?\:) .+$' @@ -199,7 +199,7 @@ jobs: accessToken: ${{ secrets.GITHUB_TOKEN }} # github access token is only required if checkAllCommitMessages is true - name: Check commits line length id: ccll - if: ${{ env.IS_PR && steps.install-deps.outcome == 'success' && !cancelled() }} + if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: pattern: '(^.{0,74}$\r?\n?){0,20}' @@ -213,8 +213,8 @@ jobs: id: commit-conformance if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} env: - CCFL_OK: ${{ (env.IS_PR && steps.ccfl.outcome == 'success') || steps.ccfl.outcome == 'skipped' }} - CCLL_OK: ${{ (env.IS_PR && steps.ccll.outcome == 'success') || steps.ccll.outcome == 'skipped' }} + CCFL_OK: ${{ (fromJSON(env.IS_PR) && steps.ccfl.outcome == 'success') || steps.ccfl.outcome == 'skipped' }} + CCLL_OK: ${{ (fromJSON(env.IS_PR) && steps.ccll.outcome == 'success') || steps.ccll.outcome == 'skipped' }} run: | if [[ "${CCFL_OK}" != "true" || "${CCLL_OK}" != "true" ]]; then echo "Issues with commits. First line ok: ${CCFL_OK}. Line length ok: ${CCLL_OK}." From c47dac833b30e7c43855c46d11e055634a07490a Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 13:22:14 +0300 Subject: [PATCH 0340/1104] refactor: replace scalars with () shaped tensors, disable python list support in inputset --- concrete/common/data_types/dtypes_helpers.py | 110 +++++------------- concrete/common/mlir/mlir_converter.py | 6 +- .../common/representation/intermediate.py | 10 +- concrete/common/values/__init__.py | 5 +- concrete/common/values/scalars.py | 54 --------- concrete/common/values/tensors.py | 57 +++++++-- concrete/numpy/__init__.py | 9 +- concrete/numpy/np_dtypes_helpers.py | 11 +- .../bounds_measurement/test_inputset_eval.py | 26 ++++- tests/common/data_types/test_values.py | 1 - tests/common/mlir/test_mlir_converter.py | 7 +- tests/numpy/test_compile.py | 6 +- tests/numpy/test_np_dtypes_helpers.py | 12 ++ 13 files changed, 134 insertions(+), 180 deletions(-) delete mode 100644 concrete/common/values/scalars.py diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 2a6f17dd0..00482d79e 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -2,18 +2,10 @@ from copy import deepcopy from functools import partial -from typing import Callable, List, Union, cast +from typing import Callable, Union, cast from ..debugging.custom_assert import custom_assert -from ..values import ( - BaseValue, - ClearScalar, - ClearTensor, - EncryptedScalar, - EncryptedTensor, - ScalarValue, - TensorValue, -) +from ..values import BaseValue, ClearTensor, EncryptedTensor, TensorValue from .base import BaseDataType from .floats import Float from .integers import Integer, get_bits_to_represent_value_as_integer @@ -24,25 +16,25 @@ BASE_DATA_TYPES = INTEGER_TYPES + FLOAT_TYPES def value_is_encrypted_scalar_integer(value_to_check: BaseValue) -> bool: - """Check that a value is an encrypted ScalarValue of type Integer. + """Check that a value is an encrypted scalar of type Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is an encrypted ScalarValue of type Integer + bool: True if the passed value_to_check is an encrypted scalar of type Integer """ return value_is_scalar_integer(value_to_check) and value_to_check.is_encrypted def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> bool: - """Check that a value is an encrypted ScalarValue of type unsigned Integer. + """Check that a value is an encrypted scalar of type unsigned Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is an encrypted ScalarValue of type Integer and + bool: True if the passed value_to_check is an encrypted scalar of type Integer and unsigned """ return ( @@ -52,28 +44,30 @@ def value_is_encrypted_scalar_unsigned_integer(value_to_check: BaseValue) -> boo def value_is_clear_scalar_integer(value_to_check: BaseValue) -> bool: - """Check that a value is a clear ScalarValue of type Integer. + """Check that a value is a clear scalar of type Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is a clear ScalarValue of type Integer + bool: True if the passed value_to_check is a clear scalar of type Integer """ return value_is_scalar_integer(value_to_check) and value_to_check.is_clear def value_is_scalar_integer(value_to_check: BaseValue) -> bool: - """Check that a value is a ScalarValue of type Integer. + """Check that a value is a scalar of type Integer. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is a ScalarValue of type Integer + bool: True if the passed value_to_check is a scalar of type Integer """ - return isinstance(value_to_check, ScalarValue) and isinstance( - value_to_check.dtype, INTEGER_TYPES + return ( + isinstance(value_to_check, TensorValue) + and value_to_check.is_scalar + and isinstance(value_to_check.dtype, INTEGER_TYPES) ) @@ -126,8 +120,10 @@ def value_is_tensor_integer(value_to_check: BaseValue) -> bool: Returns: bool: True if the passed value_to_check is a TensorValue of type Integer """ - return isinstance(value_to_check, TensorValue) and isinstance( - value_to_check.dtype, INTEGER_TYPES + return ( + isinstance(value_to_check, TensorValue) + and not value_to_check.is_scalar + and isinstance(value_to_check.dtype, INTEGER_TYPES) ) @@ -190,43 +186,6 @@ def find_type_to_hold_both_lossy( return type_to_return -def mix_scalar_values_determine_holding_dtype( - value1: ScalarValue, - value2: ScalarValue, -) -> ScalarValue: - """Return mixed ScalarValue with data type able to hold both value1 and value2 dtypes. - - Returns a ScalarValue that would result from computation on both value1 and value2 while - determining the data type able to hold both value1 and value2 data type (this can be lossy - with floats). - - Args: - value1 (ScalarValue): first ScalarValue to mix. - value2 (ScalarValue): second ScalarValue to mix. - - Returns: - ScalarValue: The resulting mixed ScalarValue with data type able to hold both value1 and - value2 dtypes. - """ - - custom_assert( - isinstance(value1, ScalarValue), f"Unsupported value1: {value1}, expected ScalarValue" - ) - custom_assert( - isinstance(value2, ScalarValue), f"Unsupported value2: {value2}, expected ScalarValue" - ) - - holding_type = find_type_to_hold_both_lossy(value1.dtype, value2.dtype) - mixed_value: ScalarValue - - if value1.is_encrypted or value2.is_encrypted: - mixed_value = EncryptedScalar(holding_type) - else: - mixed_value = ClearScalar(holding_type) - - return mixed_value - - def mix_tensor_values_determine_holding_dtype( value1: TensorValue, value2: TensorValue, @@ -284,7 +243,7 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> value2 (BaseValue): second BaseValue to mix. Raises: - ValueError: raised if the BaseValue is not one of (ScalarValue, TensorValue) + ValueError: raised if the BaseValue is not one of (TensorValue) Returns: BaseValue: The resulting mixed BaseValue with data type able to hold both value1 and value2 @@ -296,8 +255,6 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> f"Cannot mix values of different types: value 1:{type(value1)}, value2: {type(value2)}", ) - if isinstance(value1, ScalarValue) and isinstance(value2, ScalarValue): - return mix_scalar_values_determine_holding_dtype(value1, value2) if isinstance(value1, TensorValue) and isinstance(value2, TensorValue): return mix_tensor_values_determine_holding_dtype(value1, value2) @@ -306,9 +263,7 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> ) -def get_base_data_type_for_python_constant_data( - constant_data: Union[int, float, List[int], List[float]] -) -> BaseDataType: +def get_base_data_type_for_python_constant_data(constant_data: Union[int, float]) -> BaseDataType: """Determine the BaseDataType to hold the input constant data. Args: @@ -320,28 +275,23 @@ def get_base_data_type_for_python_constant_data( """ constant_data_type: BaseDataType custom_assert( - isinstance(constant_data, (int, float, list)), + isinstance(constant_data, (int, float)), f"Unsupported constant data of type {type(constant_data)}", ) - if isinstance(constant_data, list): - custom_assert(len(constant_data) > 0, "Data type of empty list cannot be detected") - constant_data_type = get_base_data_type_for_python_constant_data(constant_data[0]) - for value in constant_data: - other_data_type = get_base_data_type_for_python_constant_data(value) - constant_data_type = find_type_to_hold_both_lossy(constant_data_type, other_data_type) - elif isinstance(constant_data, int): + if isinstance(constant_data, int): is_signed = constant_data < 0 constant_data_type = Integer( get_bits_to_represent_value_as_integer(constant_data, is_signed), is_signed ) elif isinstance(constant_data, float): constant_data_type = Float(64) + return constant_data_type def get_base_value_for_python_constant_data( - constant_data: Union[int, float, List[int], List[float]] + constant_data: Union[int, float] ) -> Callable[..., BaseValue]: """Wrap the BaseDataType to hold the input constant data in BaseValue partial. @@ -349,8 +299,8 @@ def get_base_value_for_python_constant_data( by calling it with the proper arguments forwarded to the BaseValue `__init__` function Args: - constant_data (Union[int, float, List[int], List[float]]): The constant data - for which to determine the corresponding Value. + constant_data (Union[int, float]): The constant data for which to determine the + corresponding Value. Returns: Callable[..., BaseValue]: A partial object that will return the proper BaseValue when @@ -358,14 +308,8 @@ def get_base_value_for_python_constant_data( method). """ - if isinstance(constant_data, list): - assert len(constant_data) > 0 - constant_shape = (len(constant_data),) - constant_data_type = get_base_data_type_for_python_constant_data(constant_data) - return partial(TensorValue, dtype=constant_data_type, shape=constant_shape) - constant_data_type = get_base_data_type_for_python_constant_data(constant_data) - return partial(ScalarValue, dtype=constant_data_type) + return partial(TensorValue, dtype=constant_data_type, shape=()) def get_type_constructor_for_python_constant_data(constant_data: Union[int, float]): diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index 177c538dd..55af22122 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -7,7 +7,6 @@ import zamalang from mlir.dialects import builtin from mlir.ir import Context, InsertionPoint, IntegerType, Location, Module, RankedTensorType from mlir.ir import Type as MLIRType -from mlir.ir import UnrankedTensorType from zamalang.dialects import hlfhe from .. import values @@ -64,10 +63,7 @@ class MLIRConverter: MLIRType: corresponding MLIR type """ element_type = self._get_scalar_integer_type(bit_width, is_encrypted, is_signed) - if len(shape): # randked tensor - return RankedTensorType.get(shape, element_type) - # unranked tensor - return UnrankedTensorType.get(element_type) + return RankedTensorType.get(shape, element_type) def _get_scalar_integer_type( self, bit_width: int, is_encrypted: bool, is_signed: bool diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index 4ee420e72..da66a20d5 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -9,7 +9,7 @@ from loguru import logger from ..data_types.base import BaseDataType from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, - mix_scalar_values_determine_holding_dtype, + mix_values_determine_holding_dtype, ) from ..data_types.integers import Integer from ..debugging.custom_assert import custom_assert @@ -43,7 +43,7 @@ class IntermediateNode(ABC): def _init_binary( self, inputs: Iterable[BaseValue], - mix_values_func: Callable[..., BaseValue] = mix_scalar_values_determine_holding_dtype, + mix_values_func: Callable[..., BaseValue] = mix_values_determine_holding_dtype, **_kwargs, # Required to conform to __init__ typing ) -> None: """__init__ for a binary operation, ie two inputs.""" @@ -221,7 +221,11 @@ class ArbitraryFunction(IntermediateNode): self.arbitrary_func = arbitrary_func self.op_args = op_args if op_args is not None else () self.op_kwargs = op_kwargs if op_kwargs is not None else {} - self.outputs = [input_base_value.__class__(output_dtype, input_base_value.is_encrypted)] + + output = deepcopy(input_base_value) + output.dtype = output_dtype + self.outputs = [output] + self.op_name = op_name if op_name is not None else self.__class__.__name__ def evaluate(self, inputs: Dict[int, Any]) -> Any: diff --git a/concrete/common/values/__init__.py b/concrete/common/values/__init__.py index 34bc45927..4a1e3290c 100644 --- a/concrete/common/values/__init__.py +++ b/concrete/common/values/__init__.py @@ -1,6 +1,5 @@ """Module for value structures.""" -from . import scalars, tensors +from . import tensors from .base import BaseValue -from .scalars import ClearScalar, EncryptedScalar, ScalarValue -from .tensors import ClearTensor, EncryptedTensor, TensorValue +from .tensors import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor, TensorValue diff --git a/concrete/common/values/scalars.py b/concrete/common/values/scalars.py deleted file mode 100644 index 66be4eb26..000000000 --- a/concrete/common/values/scalars.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Module that defines the scalar values in a program.""" - -from typing import Tuple - -from ..data_types.base import BaseDataType -from .base import BaseValue - - -class ScalarValue(BaseValue): - """Class representing a scalar value.""" - - def __eq__(self, other: object) -> bool: - return BaseValue.__eq__(self, other) - - def __str__(self) -> str: # pragma: no cover - encrypted_str = "Encrypted" if self._is_encrypted else "Clear" - return f"{encrypted_str}Scalar<{self.dtype!r}>" - - @property - def shape(self) -> Tuple[int, ...]: - """Return the ScalarValue shape property. - - Returns: - Tuple[int, ...]: The ScalarValue shape which is `()`. - """ - return () - - -def make_clear_scalar(dtype: BaseDataType) -> ScalarValue: - """Create a clear ScalarValue. - - Args: - dtype (BaseDataType): The data type for the value. - - Returns: - ScalarValue: The corresponding ScalarValue. - """ - return ScalarValue(dtype=dtype, is_encrypted=False) - - -def make_encrypted_scalar(dtype: BaseDataType) -> ScalarValue: - """Create an encrypted ScalarValue. - - Args: - dtype (BaseDataType): The data type for the value. - - Returns: - ScalarValue: The corresponding ScalarValue. - """ - return ScalarValue(dtype=dtype, is_encrypted=True) - - -ClearScalar = make_clear_scalar -EncryptedScalar = make_encrypted_scalar diff --git a/concrete/common/values/tensors.py b/concrete/common/values/tensors.py index ded52565e..5a6eb8185 100644 --- a/concrete/common/values/tensors.py +++ b/concrete/common/values/tensors.py @@ -1,7 +1,7 @@ """Module that defines the tensor values in a program.""" from math import prod -from typing import Optional, Tuple +from typing import Tuple from ..data_types.base import BaseDataType from .base import BaseValue @@ -18,13 +18,13 @@ class TensorValue(BaseValue): self, dtype: BaseDataType, is_encrypted: bool, - shape: Optional[Tuple[int, ...]] = None, - ) -> None: + shape: Tuple[int, ...], + ): super().__init__(dtype, is_encrypted) - # Managing tensors as in numpy, no shape or () is treated as a 0-D array of size 1 - self._shape = shape if shape is not None else () + # Managing tensors as in numpy, shape of () means the value is scalar + self._shape = shape self._ndim = len(self._shape) - self._size = prod(self._shape) if self._shape else 1 + self._size = prod(self._shape) if self._shape != () else 1 def __eq__(self, other: object) -> bool: return ( @@ -37,7 +37,9 @@ class TensorValue(BaseValue): def __str__(self) -> str: encrypted_str = "Encrypted" if self._is_encrypted else "Clear" - return f"{encrypted_str}Tensor<{str(self.dtype)}, shape={self.shape}>" + tensor_or_scalar_str = "Scalar" if self.is_scalar else "Tensor" + shape_str = f", shape={self.shape}" if self.shape != () else "" + return f"{encrypted_str}{tensor_or_scalar_str}<{str(self.dtype)}{shape_str}>" @property def shape(self) -> Tuple[int, ...]: @@ -66,10 +68,19 @@ class TensorValue(BaseValue): """ return self._size + @property + def is_scalar(self) -> bool: + """Whether Value is scalar or not. + + Returns: + bool: True if scalar False otherwise + """ + return self.shape == () + def make_clear_tensor( dtype: BaseDataType, - shape: Optional[Tuple[int, ...]] = None, + shape: Tuple[int, ...], ) -> TensorValue: """Create a clear TensorValue. @@ -85,7 +96,7 @@ def make_clear_tensor( def make_encrypted_tensor( dtype: BaseDataType, - shape: Optional[Tuple[int, ...]] = None, + shape: Tuple[int, ...], ) -> TensorValue: """Create an encrypted TensorValue. @@ -101,3 +112,31 @@ def make_encrypted_tensor( ClearTensor = make_clear_tensor EncryptedTensor = make_encrypted_tensor + + +def make_clear_scalar(dtype: BaseDataType) -> TensorValue: + """Create a clear scalar value. + + Args: + dtype (BaseDataType): The data type for the value. + + Returns: + TensorValue: The corresponding TensorValue. + """ + return TensorValue(dtype=dtype, is_encrypted=False, shape=()) + + +def make_encrypted_scalar(dtype: BaseDataType) -> TensorValue: + """Create an encrypted scalar value. + + Args: + dtype (BaseDataType): The data type for the value. + + Returns: + TensorValue: The corresponding TensorValue. + """ + return TensorValue(dtype=dtype, is_encrypted=True, shape=()) + + +ClearScalar = make_clear_scalar +EncryptedScalar = make_encrypted_scalar diff --git a/concrete/numpy/__init__.py b/concrete/numpy/__init__.py index 7e793806f..8adaa8c99 100644 --- a/concrete/numpy/__init__.py +++ b/concrete/numpy/__init__.py @@ -4,13 +4,6 @@ from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.data_types import Float, Float32, Float64, Integer, SignedInteger, UnsignedInteger from ..common.debugging import draw_graph, get_printable_graph from ..common.extensions.table import LookupTable -from ..common.values import ( - ClearScalar, - ClearTensor, - EncryptedScalar, - EncryptedTensor, - ScalarValue, - TensorValue, -) +from ..common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor, TensorValue from .compile import compile_numpy_function, compile_numpy_function_into_op_graph from .tracing import trace_numpy_function diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 72d3b52f2..8f61998cc 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -18,7 +18,7 @@ from ..common.data_types.dtypes_helpers import ( from ..common.data_types.floats import Float from ..common.data_types.integers import Integer from ..common.debugging.custom_assert import custom_assert -from ..common.values import BaseValue, ScalarValue, TensorValue +from ..common.values import BaseValue, TensorValue NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.int32): Integer(32, is_signed=True), @@ -158,10 +158,15 @@ def get_base_value_for_numpy_or_python_constant_data( with `encrypted` as keyword argument (forwarded to the BaseValue `__init__` method). """ constant_data_value: Callable[..., BaseValue] + custom_assert( + not isinstance(constant_data, list), + "Unsupported constant data of type list " + "(if you meant to use a list as an array, please use numpy.array instead)", + ) custom_assert( isinstance( constant_data, - (int, float, list, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES), + (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES), ), f"Unsupported constant data of type {type(constant_data)}", ) @@ -170,7 +175,7 @@ def get_base_value_for_numpy_or_python_constant_data( if isinstance(constant_data, numpy.ndarray): constant_data_value = partial(TensorValue, dtype=base_dtype, shape=constant_data.shape) elif isinstance(constant_data, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES): - constant_data_value = partial(ScalarValue, dtype=base_dtype) + constant_data_value = partial(TensorValue, dtype=base_dtype, shape=()) else: constant_data_value = get_base_value_for_python_constant_data(constant_data) return constant_data_value diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index d622e9ee7..d977fce93 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -309,14 +309,21 @@ def test_eval_op_graph_bounds_on_non_conformant_inputset_default(capsys): y = ClearTensor(UnsignedInteger(2), (3,)) inputset = [ - ([2, 1, 3, 1], [1, 2, 1, 1]), - ([3, 3, 3], [3, 3, 5]), + (np.array([2, 1, 3, 1]), np.array([1, 2, 1, 1])), + (np.array([3, 3, 3]), np.array([3, 3, 5])), ] op_graph = trace_numpy_function(f, {"x": x, "y": y}) configuration = CompilationConfiguration() - eval_op_graph_bounds_on_inputset(op_graph, inputset, compilation_configuration=configuration) + eval_op_graph_bounds_on_inputset( + op_graph, + inputset, + compilation_configuration=configuration, + min_func=numpy_min_func, + max_func=numpy_max_func, + get_base_value_for_constant_data_func=get_base_value_for_numpy_or_python_constant_data, + ) captured = capsys.readouterr() assert ( @@ -339,14 +346,21 @@ def test_eval_op_graph_bounds_on_non_conformant_inputset_check_all(capsys): y = ClearTensor(UnsignedInteger(2), (3,)) inputset = [ - ([2, 1, 3, 1], [1, 2, 1, 1]), - ([3, 3, 3], [3, 3, 5]), + (np.array([2, 1, 3, 1]), np.array([1, 2, 1, 1])), + (np.array([3, 3, 3]), np.array([3, 3, 5])), ] op_graph = trace_numpy_function(f, {"x": x, "y": y}) configuration = CompilationConfiguration(check_every_input_in_inputset=True) - eval_op_graph_bounds_on_inputset(op_graph, inputset, compilation_configuration=configuration) + eval_op_graph_bounds_on_inputset( + op_graph, + inputset, + compilation_configuration=configuration, + min_func=numpy_min_func, + max_func=numpy_max_func, + get_base_value_for_constant_data_func=get_base_value_for_numpy_or_python_constant_data, + ) captured = capsys.readouterr() assert ( diff --git a/tests/common/data_types/test_values.py b/tests/common/data_types/test_values.py index 6f18a59ec..25c24f488 100644 --- a/tests/common/data_types/test_values.py +++ b/tests/common/data_types/test_values.py @@ -31,7 +31,6 @@ class DummyDtype(BaseDataType): @pytest.mark.parametrize( "shape,expected_shape,expected_ndim,expected_size", [ - (None, (), 0, 1), ((), (), 0, 1), ((3, 256, 256), (3, 256, 256), 3, 196_608), ((1920, 1080, 3), (1920, 1080, 3), 3, 6_220_800), diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 2ab61b91a..305d879ca 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -256,7 +256,10 @@ def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): result_graph = compile_numpy_function_into_op_graph( func, args_dict, - (([data[0]] * n, [data[1]] * n) for data in datagen(*args_ranges)), + ( + (numpy.array([data[0]] * n), numpy.array([data[1]] * n)) + for data in datagen(*args_ranges) + ), ) converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) @@ -289,7 +292,6 @@ def test_concrete_clear_integer_to_mlir_type(is_signed): @pytest.mark.parametrize( "shape", [ - None, (5,), (5, 8), (-1, 5), @@ -315,7 +317,6 @@ def test_concrete_clear_tensor_integer_to_mlir_type(is_signed, shape): @pytest.mark.parametrize( "shape", [ - None, (5,), (5, 8), (-1, 5), diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 5f30b9838..8f141ae90 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -176,7 +176,9 @@ def test_compile_and_run_dot_correctness(size, input_range): def data_gen(input_range, size): for _ in range(1000): low, high = input_range - args = [[random.randint(low, high) for _ in range(size)] for __ in range(2)] + args = [ + numpy.array([random.randint(low, high) for _ in range(size)]) for __ in range(2) + ] yield args @@ -303,7 +305,7 @@ def test_compile_function_with_dot(function, params, shape, ref_graph_str): iter_i = itertools.product(range(0, max_for_ij + 1), repeat=repeat) iter_j = itertools.product(range(0, max_for_ij + 1), repeat=repeat) for prod_i, prod_j in itertools.product(iter_i, iter_j): - yield (list(prod_i), list(prod_j)) + yield numpy.array(prod_i), numpy.array(prod_j) max_for_ij = 3 assert len(shape) == 1 diff --git a/tests/numpy/test_np_dtypes_helpers.py b/tests/numpy/test_np_dtypes_helpers.py index d48180657..a10e2b594 100644 --- a/tests/numpy/test_np_dtypes_helpers.py +++ b/tests/numpy/test_np_dtypes_helpers.py @@ -8,6 +8,7 @@ from concrete.common.data_types.integers import Integer from concrete.numpy.np_dtypes_helpers import ( convert_base_data_type_to_numpy_dtype, convert_numpy_dtype_to_base_data_type, + get_base_value_for_numpy_or_python_constant_data, get_type_constructor_for_numpy_or_python_constant_data, ) @@ -76,3 +77,14 @@ def test_get_type_constructor_for_numpy_or_python_constant_data( assert expected_constructor == get_type_constructor_for_numpy_or_python_constant_data( constant_data ) + + +def test_get_base_value_for_numpy_or_python_constant_data_with_list(): + """Test function for get_base_value_for_numpy_or_python_constant_data called with list""" + + with pytest.raises( + AssertionError, + match="Unsupported constant data of type list " + "\\(if you meant to use a list as an array, please use numpy\\.array instead\\)", + ): + get_base_value_for_numpy_or_python_constant_data([1, 2, 3]) From bd95714c239ffa7aeed386cf16e068ee0c251d73 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 29 Sep 2021 17:15:18 +0300 Subject: [PATCH 0341/1104] feat: add more metrics to linear regression benchmark --- benchmarks/linear_regression.py | 37 ++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index 1f9f4327f..f94413e76 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -1,5 +1,9 @@ # Target: Linear Regression +# Disable line length warnings as we have a looooong metric... +# flake8: noqa: E501 +# pylint: disable=C0301 + import numpy as np import concrete.numpy as hnp @@ -149,17 +153,40 @@ def main(): ) # Measure: End - loss = 0 - for x_i, y_i in zip(x_q, y): + non_homomorphic_loss = 0 + homomorphic_loss = 0 + + for i, (x_i, y_i) in enumerate(zip(x_q, y)): x_i = [int(value) for value in x_i] + non_homomorphic_prediction = model.evaluate(x[i])[0] # Measure: Evaluation Time (ms) - prediction = QuantizedArray(engine.run(*x_i), y_parameters).dequantize() + homomorphic_prediction = QuantizedArray(engine.run(*x_i), y_parameters).dequantize() # Measure: End - loss += (prediction - y_i) ** 2 + non_homomorphic_loss += (non_homomorphic_prediction - y_i) ** 2 + homomorphic_loss += (homomorphic_prediction - y_i) ** 2 - # Measure: Loss = loss / len(y) + print() + + print(f"input = {x[i][0]}") + print(f"output = {y_i:.4f}") + + print(f"non homomorphic prediction = {non_homomorphic_loss:.4f}") + print(f"homomorphic prediction = {homomorphic_prediction:.4f}") + + non_homomorphic_loss /= len(y) + homomorphic_loss /= len(y) + difference = abs(homomorphic_loss - non_homomorphic_loss) * 100 / non_homomorphic_loss + + print() + print(f"Non Homomorphic Loss: {non_homomorphic_loss:.4f}") + print(f"Homomorphic Loss: {homomorphic_loss:.4f}") + print(f"Relative Difference Percentage: {difference:.2f}%") + + # Measure: Non Homomorphic Loss = non_homomorphic_loss + # Measure: Homomorphic Loss = homomorphic_loss + # Measure: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference if __name__ == "__main__": From 0a758ed6727edad4575ac4565993eb083feeffd7 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 11:27:21 +0200 Subject: [PATCH 0342/1104] test: change a bit the way we test to prepare modifications for more ufunc refs #126 --- tests/numpy/test_tracing.py | 107 +++++++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 8 deletions(-) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index b855820f1..b554c30ee 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -231,38 +231,125 @@ def test_tracing_astype( @pytest.mark.parametrize( - "inputs,expected_output_node,expected_output_value", + "inputs,expected_output_node", [ pytest.param( {"x": EncryptedScalar(Integer(7, is_signed=False))}, ir.ArbitraryFunction, - EncryptedScalar(Float(64)), ), pytest.param( {"x": EncryptedScalar(Integer(32, is_signed=True))}, ir.ArbitraryFunction, - EncryptedScalar(Float(64)), ), pytest.param( {"x": EncryptedScalar(Integer(64, is_signed=True))}, ir.ArbitraryFunction, - EncryptedScalar(Float(64)), ), pytest.param( {"x": EncryptedScalar(Integer(128, is_signed=True))}, ir.ArbitraryFunction, - None, marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), ), pytest.param( {"x": EncryptedScalar(Float(64))}, ir.ArbitraryFunction, - EncryptedScalar(Float(64)), ), ], ) -def test_trace_numpy_supported_ufuncs(inputs, expected_output_node, expected_output_value): +def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): """Function to trace supported numpy ufuncs""" + + LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: List[numpy.ufunc] = [ + # The commented functions are functions which don't work for the moment, often + # if not always because they require more than a single argument + # numpy.absolute, + # numpy.add, + numpy.arccos, + numpy.arccosh, + numpy.arcsin, + numpy.arcsinh, + numpy.arctan, + # numpy.arctan2, + numpy.arctanh, + # numpy.bitwise_and, + # numpy.bitwise_or, + # numpy.bitwise_xor, + numpy.cbrt, + numpy.ceil, + # numpy.conjugate, + # numpy.copysign, + numpy.cos, + numpy.cosh, + numpy.deg2rad, + numpy.degrees, + # numpy.divmod, + # numpy.equal, + numpy.exp, + numpy.exp2, + numpy.expm1, + numpy.fabs, + # numpy.float_power, + numpy.floor, + # numpy.floor_divide, + # numpy.fmax, + # numpy.fmin, + # numpy.fmod, + # numpy.frexp, + # numpy.gcd, + # numpy.greater, + # numpy.greater_equal, + # numpy.heaviside, + # numpy.hypot, + # numpy.invert, + # numpy.isfinite, + # numpy.isinf, + # numpy.isnan, + # numpy.isnat, + # numpy.lcm, + # numpy.ldexp, + # numpy.left_shift, + # numpy.less, + # numpy.less_equal, + numpy.log, + numpy.log10, + numpy.log1p, + numpy.log2, + # numpy.logaddexp, + # numpy.logaddexp2, + # numpy.logical_and, + # numpy.logical_not, + # numpy.logical_or, + # numpy.logical_xor, + # numpy.matmul, + # numpy.maximum, + # numpy.minimum, + # numpy.modf, + # numpy.multiply, + # numpy.negative, + # numpy.nextafter, + # numpy.not_equal, + # numpy.positive, + # numpy.power, + numpy.rad2deg, + numpy.radians, + # numpy.reciprocal, + # numpy.remainder, + # numpy.right_shift, + numpy.rint, + # numpy.sign, + # numpy.signbit, + numpy.sin, + numpy.sinh, + numpy.spacing, + numpy.sqrt, + # numpy.square, + # numpy.subtract, + numpy.tan, + numpy.tanh, + # numpy.true_divide, + numpy.trunc, + ] + for function_to_trace_def in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: # We really need a lambda (because numpy functions are not playing @@ -277,7 +364,11 @@ def test_trace_numpy_supported_ufuncs(inputs, expected_output_node, expected_out assert len(op_graph.output_nodes) == 1 assert isinstance(op_graph.output_nodes[0], expected_output_node) assert len(op_graph.output_nodes[0].outputs) == 1 - assert op_graph.output_nodes[0].outputs[0] == expected_output_value + + if function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: + assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) + else: + assert op_graph.output_nodes[0].outputs[0] == "to be done" def test_trace_numpy_ufuncs_not_supported(): From 2351730cee436fbe104942e681896193dd5f2b84 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 11:44:04 +0200 Subject: [PATCH 0343/1104] feat: adding management of numpy.absolute refs #126 --- concrete/numpy/tracing.py | 2 +- tests/numpy/test_tracing.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 6198b7ca7..24e19f74b 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -191,7 +191,7 @@ class NPTracer(BaseTracer): LIST_OF_SUPPORTED_UFUNC: List[numpy.ufunc] = [ # The commented functions are functions which don't work for the moment, often # if not always because they require more than a single argument - # numpy.absolute, + numpy.absolute, # numpy.add, numpy.arccos, numpy.arccosh, diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index b554c30ee..aa47196b0 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -368,7 +368,12 @@ def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): if function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) else: - assert op_graph.output_nodes[0].outputs[0] == "to be done" + assert op_graph.output_nodes[0].outputs[0] in [ + EncryptedScalar(Integer(32, is_signed=False)), + EncryptedScalar(Integer(32, is_signed=True)), + EncryptedScalar(Integer(64, is_signed=True)), + EncryptedScalar(Float(64)), + ] def test_trace_numpy_ufuncs_not_supported(): From ddf08eb2738dd4610eebadf84e6d41d2b449ebf1 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 11:45:30 +0200 Subject: [PATCH 0344/1104] feat: adding management of numpy.square refs #126 --- concrete/numpy/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 24e19f74b..c62068f7f 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -271,7 +271,7 @@ class NPTracer(BaseTracer): numpy.sinh, numpy.spacing, numpy.sqrt, - # numpy.square, + numpy.square, # numpy.subtract, numpy.tan, numpy.tanh, From ab7cf24285e266d2c16b48b033de692e06287491 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:07:12 +0200 Subject: [PATCH 0345/1104] fix: pylint. --- tests/numpy/test_tracing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index aa47196b0..aa08ab00e 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -259,7 +259,7 @@ def test_tracing_astype( def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): """Function to trace supported numpy ufuncs""" - LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: List[numpy.ufunc] = [ + list_of_ufunc_whose_output_is_float64 = [ # The commented functions are functions which don't work for the moment, often # if not always because they require more than a single argument # numpy.absolute, @@ -365,7 +365,7 @@ def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): assert isinstance(op_graph.output_nodes[0], expected_output_node) assert len(op_graph.output_nodes[0].outputs) == 1 - if function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: + if function_to_trace_def in list_of_ufunc_whose_output_is_float64: assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) else: assert op_graph.output_nodes[0].outputs[0] in [ From 4adb0eb18e58fd8548ab935a569b5258b9806329 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:24:49 +0200 Subject: [PATCH 0346/1104] feat: adding management of numpy.isfinite, numpy.isinf, numpy.isnan refs #126 --- concrete/numpy/np_dtypes_helpers.py | 1 + concrete/numpy/tracing.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 8f61998cc..e974bc8ab 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -27,6 +27,7 @@ NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.uint64): Integer(64, is_signed=False), numpy.dtype(numpy.float32): Float(32), numpy.dtype(numpy.float64): Float(64), + numpy.dtype(bool): Integer(32, is_signed=False), } SUPPORTED_NUMPY_DTYPES = tuple(NUMPY_TO_COMMON_DTYPE_MAPPING) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index c62068f7f..f80e8de8d 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -230,9 +230,9 @@ class NPTracer(BaseTracer): # numpy.heaviside, # numpy.hypot, # numpy.invert, - # numpy.isfinite, - # numpy.isinf, - # numpy.isnan, + numpy.isfinite, + numpy.isinf, + numpy.isnan, # numpy.isnat, # numpy.lcm, # numpy.ldexp, From 2638ba59acdd94ab98584b20f5b2919695bd9a0d Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:28:09 +0200 Subject: [PATCH 0347/1104] feat: adding management of numpy.negative refs #126 --- concrete/numpy/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index f80e8de8d..6ed756d40 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -254,7 +254,7 @@ class NPTracer(BaseTracer): # numpy.minimum, # numpy.modf, # numpy.multiply, - # numpy.negative, + numpy.negative, # numpy.nextafter, # numpy.not_equal, # numpy.positive, From c52032c28525d44edc808a4b35d87a5808fd9c10 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:32:33 +0200 Subject: [PATCH 0348/1104] feat: adding management of numpy.positive refs #126 --- concrete/numpy/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 6ed756d40..ebda617a1 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -257,7 +257,7 @@ class NPTracer(BaseTracer): numpy.negative, # numpy.nextafter, # numpy.not_equal, - # numpy.positive, + numpy.positive, # numpy.power, numpy.rad2deg, numpy.radians, From 406043575a20798d0679af48f57a020d121b0770 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:34:50 +0200 Subject: [PATCH 0349/1104] feat: adding management of numpy.reciprocal refs #126 --- concrete/numpy/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index ebda617a1..99023884c 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -261,7 +261,7 @@ class NPTracer(BaseTracer): # numpy.power, numpy.rad2deg, numpy.radians, - # numpy.reciprocal, + numpy.reciprocal, # numpy.remainder, # numpy.right_shift, numpy.rint, From 2b7fe094d28adfe7e9962a98749af70f53e64615 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:36:20 +0200 Subject: [PATCH 0350/1104] feat: adding management of numpy.sign and numpy.signbit refs #126 --- concrete/numpy/tracing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 99023884c..8c3a66e46 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -265,8 +265,8 @@ class NPTracer(BaseTracer): # numpy.remainder, # numpy.right_shift, numpy.rint, - # numpy.sign, - # numpy.signbit, + numpy.sign, + numpy.signbit, numpy.sin, numpy.sinh, numpy.spacing, From e67e19ab4f453c72877ab508dca3be28e0021323 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 14:59:04 +0200 Subject: [PATCH 0351/1104] fix: comments. --- concrete/numpy/tracing.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 8c3a66e46..ddcb55702 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -189,8 +189,7 @@ class NPTracer(BaseTracer): return output_tracer LIST_OF_SUPPORTED_UFUNC: List[numpy.ufunc] = [ - # The commented functions are functions which don't work for the moment, often - # if not always because they require more than a single argument + # The commented functions are functions require more than a single argument numpy.absolute, # numpy.add, numpy.arccos, From 7ea39fb77df478fa81da267c48c13dca6a26a5ae Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 15:20:21 +0200 Subject: [PATCH 0352/1104] fix: clear comments --- tests/numpy/test_tracing.py | 59 ++----------------------------------- 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index aa08ab00e..0b9b4c42b 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -259,94 +259,39 @@ def test_tracing_astype( def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): """Function to trace supported numpy ufuncs""" + # Functions from tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC, whose output + # is a float64, whatever the input type list_of_ufunc_whose_output_is_float64 = [ - # The commented functions are functions which don't work for the moment, often - # if not always because they require more than a single argument - # numpy.absolute, - # numpy.add, numpy.arccos, numpy.arccosh, numpy.arcsin, numpy.arcsinh, numpy.arctan, - # numpy.arctan2, numpy.arctanh, - # numpy.bitwise_and, - # numpy.bitwise_or, - # numpy.bitwise_xor, numpy.cbrt, numpy.ceil, - # numpy.conjugate, - # numpy.copysign, numpy.cos, numpy.cosh, numpy.deg2rad, numpy.degrees, - # numpy.divmod, - # numpy.equal, numpy.exp, numpy.exp2, numpy.expm1, numpy.fabs, - # numpy.float_power, numpy.floor, - # numpy.floor_divide, - # numpy.fmax, - # numpy.fmin, - # numpy.fmod, - # numpy.frexp, - # numpy.gcd, - # numpy.greater, - # numpy.greater_equal, - # numpy.heaviside, - # numpy.hypot, - # numpy.invert, - # numpy.isfinite, - # numpy.isinf, - # numpy.isnan, - # numpy.isnat, - # numpy.lcm, - # numpy.ldexp, - # numpy.left_shift, - # numpy.less, - # numpy.less_equal, numpy.log, numpy.log10, numpy.log1p, numpy.log2, - # numpy.logaddexp, - # numpy.logaddexp2, - # numpy.logical_and, - # numpy.logical_not, - # numpy.logical_or, - # numpy.logical_xor, - # numpy.matmul, - # numpy.maximum, - # numpy.minimum, - # numpy.modf, - # numpy.multiply, - # numpy.negative, - # numpy.nextafter, - # numpy.not_equal, - # numpy.positive, - # numpy.power, numpy.rad2deg, numpy.radians, - # numpy.reciprocal, - # numpy.remainder, - # numpy.right_shift, numpy.rint, - # numpy.sign, - # numpy.signbit, numpy.sin, numpy.sinh, numpy.spacing, numpy.sqrt, - # numpy.square, - # numpy.subtract, numpy.tan, numpy.tanh, - # numpy.true_divide, numpy.trunc, ] From 3d6baf410187b074d9371ee1b88c27e4e78f4e29 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 15:42:52 +0200 Subject: [PATCH 0353/1104] fix: fix the type --- concrete/numpy/np_dtypes_helpers.py | 2 +- tests/numpy/test_tracing.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index e974bc8ab..dc5a0d3bf 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -27,7 +27,7 @@ NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { numpy.dtype(numpy.uint64): Integer(64, is_signed=False), numpy.dtype(numpy.float32): Float(32), numpy.dtype(numpy.float64): Float(64), - numpy.dtype(bool): Integer(32, is_signed=False), + numpy.dtype(bool): Integer(8, is_signed=False), } SUPPORTED_NUMPY_DTYPES = tuple(NUMPY_TO_COMMON_DTYPE_MAPPING) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 0b9b4c42b..2ffca1a90 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -314,6 +314,7 @@ def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) else: assert op_graph.output_nodes[0].outputs[0] in [ + EncryptedScalar(Integer(8, is_signed=False)), EncryptedScalar(Integer(32, is_signed=False)), EncryptedScalar(Integer(32, is_signed=True)), EncryptedScalar(Integer(64, is_signed=True)), From 42d5b66b69f883f848f5f682ae859830f6bd218a Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 29 Sep 2021 19:21:12 +0200 Subject: [PATCH 0354/1104] fix: better test. --- tests/numpy/test_tracing.py | 113 ++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 2ffca1a90..26a93d562 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -1,5 +1,7 @@ """Test file for numpy tracing""" +from copy import deepcopy + import networkx as nx import numpy import pytest @@ -12,6 +14,55 @@ from concrete.numpy import tracing OPERATIONS_TO_TEST = [ir.Add, ir.Sub, ir.Mul] +# Functions from tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC, whose output +# is a float64, whatever the input type +LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64 = set( + [ + numpy.arccos, + numpy.arccosh, + numpy.arcsin, + numpy.arcsinh, + numpy.arctan, + numpy.arctanh, + numpy.cbrt, + numpy.ceil, + numpy.cos, + numpy.cosh, + numpy.deg2rad, + numpy.degrees, + numpy.exp, + numpy.exp2, + numpy.expm1, + numpy.fabs, + numpy.floor, + numpy.log, + numpy.log10, + numpy.log1p, + numpy.log2, + numpy.rad2deg, + numpy.radians, + numpy.rint, + numpy.sin, + numpy.sinh, + numpy.spacing, + numpy.sqrt, + numpy.tan, + numpy.tanh, + numpy.trunc, + ] +) + +# Functions from tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC, whose output +# is a boolean, whatever the input type +LIST_OF_UFUNC_WHOSE_OUTPUT_IS_BOOL = set( + [ + numpy.isfinite, + numpy.isinf, + numpy.isnan, + numpy.signbit, + ] +) + @pytest.mark.parametrize( "operation", @@ -259,42 +310,6 @@ def test_tracing_astype( def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): """Function to trace supported numpy ufuncs""" - # Functions from tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC, whose output - # is a float64, whatever the input type - list_of_ufunc_whose_output_is_float64 = [ - numpy.arccos, - numpy.arccosh, - numpy.arcsin, - numpy.arcsinh, - numpy.arctan, - numpy.arctanh, - numpy.cbrt, - numpy.ceil, - numpy.cos, - numpy.cosh, - numpy.deg2rad, - numpy.degrees, - numpy.exp, - numpy.exp2, - numpy.expm1, - numpy.fabs, - numpy.floor, - numpy.log, - numpy.log10, - numpy.log1p, - numpy.log2, - numpy.rad2deg, - numpy.radians, - numpy.rint, - numpy.sin, - numpy.sinh, - numpy.spacing, - numpy.sqrt, - numpy.tan, - numpy.tanh, - numpy.trunc, - ] - for function_to_trace_def in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: # We really need a lambda (because numpy functions are not playing @@ -310,16 +325,26 @@ def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): assert isinstance(op_graph.output_nodes[0], expected_output_node) assert len(op_graph.output_nodes[0].outputs) == 1 - if function_to_trace_def in list_of_ufunc_whose_output_is_float64: + if function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) + elif function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_BOOL: + + # Boolean function + assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar( + Integer(8, is_signed=False) + ) else: - assert op_graph.output_nodes[0].outputs[0] in [ - EncryptedScalar(Integer(8, is_signed=False)), - EncryptedScalar(Integer(32, is_signed=False)), - EncryptedScalar(Integer(32, is_signed=True)), - EncryptedScalar(Integer(64, is_signed=True)), - EncryptedScalar(Float(64)), - ] + + # Function keeping more or less input type + input_node_type = inputs["x"] + + expected_output_node_type = deepcopy(input_node_type) + + expected_output_node_type.dtype.bit_width = max( + expected_output_node_type.dtype.bit_width, 32 + ) + + assert op_graph.output_nodes[0].outputs[0] == expected_output_node_type def test_trace_numpy_ufuncs_not_supported(): From a9d44f4758b61e616b2df31865d239fb045dff12 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 30 Sep 2021 10:00:37 +0200 Subject: [PATCH 0355/1104] feat(float_fusing): restrict to scalars before supporting tensors --- concrete/common/optimization/topological.py | 22 +++++++++++++ .../common/optimization/test_float_fusing.py | 32 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index ab62a940a..77bd5f947 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -10,6 +10,7 @@ from ..data_types.integers import Integer from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph from ..representation.intermediate import ArbitraryFunction, Constant, Input, IntermediateNode +from ..values import TensorValue def fuse_float_operations( @@ -38,6 +39,10 @@ def fuse_float_operations( float_subgraph_start_nodes, terminal_node, subgraph_all_nodes = float_subgraph_search_result processed_terminal_nodes.add(terminal_node) + # TODO: #199 To be removed when doing tensor management + if not subgraph_is_scalar_only(subgraph_all_nodes): + continue + subgraph_conversion_result = convert_float_subgraph_to_fused_node( op_graph, float_subgraph_start_nodes, @@ -239,6 +244,23 @@ def find_float_subgraph_with_unique_terminal_node( return float_subgraph_start_nodes, terminal_node, subgraph_all_nodes +# TODO: #199 To be removed when doing tensor management +def subgraph_is_scalar_only(subgraph_all_nodes: Set[IntermediateNode]) -> bool: + """Check subgraph only processes scalars. + + Args: + subgraph_all_nodes (Set[IntermediateNode]): The nodes of the float subgraph. + + Returns: + bool: True if all inputs and outputs of the nodes in the subgraph are scalars. + """ + return all( + all(isinstance(input_, TensorValue) and input_.is_scalar for input_ in node.inputs) + and all(isinstance(output, TensorValue) and output.is_scalar for output in node.outputs) + for node in subgraph_all_nodes + ) + + def subgraph_has_unique_variable_input( float_subgraph_start_nodes: Set[IntermediateNode], ) -> bool: diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 57cef96d3..f10535aab 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -1,5 +1,6 @@ """Test file for float subgraph fusing""" +import random from inspect import signature import numpy @@ -7,7 +8,7 @@ import pytest from concrete.common.data_types.integers import Integer from concrete.common.optimization.topological import fuse_float_operations -from concrete.common.values import EncryptedScalar +from concrete.common.values import EncryptedScalar, EncryptedTensor from concrete.numpy import tracing from concrete.numpy.tracing import trace_numpy_function @@ -116,6 +117,35 @@ def test_fuse_float_operations(function_to_trace, fused, input_): assert function_to_trace(*inputs) == op_graph(*inputs) +# TODO: #199 To be removed when doing tensor management +def test_tensor_no_fuse(): + """Test case to verify float fusing is only applied on functions on scalars.""" + + ndim = random.randint(1, 3) + tensor_shape = tuple(random.randint(1, 10) for _ in range(ndim + 1)) + + def tensor_no_fuse(x): + intermediate = x.astype(numpy.float64) + intermediate = intermediate.astype(numpy.int32) + return intermediate + numpy.ones(tensor_shape) + + function_to_trace = tensor_no_fuse + params_names = signature(function_to_trace).parameters.keys() + + op_graph = trace_numpy_function( + function_to_trace, + { + param_name: EncryptedTensor(Integer(32, True), shape=tensor_shape) + for param_name in params_names + }, + ) + orig_num_nodes = len(op_graph.graph) + fuse_float_operations(op_graph) + fused_num_nodes = len(op_graph.graph) + + assert orig_num_nodes == fused_num_nodes + + def test_fuse_float_operations_correctness(): """Test functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC with fuse_float_operations.""" From edcbc0cffd962968f6fdb12d0bb1385d650c5bc3 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 30 Sep 2021 12:34:18 +0300 Subject: [PATCH 0356/1104] fix: select correct parameters for dequantization after evaluation in logistic regression benchmark --- benchmarks/logistic_regression.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index b91a91a4f..dc8bc9bb9 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -158,14 +158,17 @@ def main(): intermediate = x @ w + b intermediate_q = x_q.affine(w_q, b_q, intermediate.min(), intermediate.max(), output_bits) - n_y = output_bits - q_y = (2 ** output_bits - 1) / (intermediate.max() - intermediate.min()) - zp_y = int(round(intermediate.min() * q_y)) - y_parameters = QuantizationParameters(q_y, zp_y, n_y) + sigmoid = QuantizedFunction.plain( + lambda x: 1 / (1 + np.exp(-x)), intermediate_q.parameters, output_bits + ) + + y_q = sigmoid.apply(intermediate_q) + y_parameters = y_q.parameters q_x = x_q.parameters.q q_w = w_q.parameters.q q_b = b_q.parameters.q + q_intermediate = intermediate_q.parameters.q zp_x = x_q.parameters.zp zp_w = w_q.parameters.zp @@ -175,10 +178,10 @@ def main(): w_q = w_q.values b_q = b_q.values - c1 = q_y / (q_x * q_w) + c1 = q_intermediate / (q_x * q_w) c2 = w_q + zp_w c3 = (q_x * q_w / q_b) * (b_q + zp_b) - c4 = intermediate.min() * q_y + c4 = intermediate.min() * q_intermediate def f(x): values = ((c1 * (x + c3)) - c4).round().clip(0, 2 ** output_bits - 1).astype(np.uint) @@ -229,7 +232,10 @@ def main(): if prediction == y_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(y)) * 100 + accuracy = (correct / len(y)) * 100 + print(f"Accuracy: {accuracy:.2f}%") + + # Measure: Accuracy (%) = accuracy if __name__ == "__main__": From 6fc6991839fcbb056b9941cfe44c6b71dae9a067 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 30 Sep 2021 16:21:15 +0200 Subject: [PATCH 0357/1104] chore: add __version__ and automated tools to update it - also add a checker to verify that versions are in sync --- .github/ISSUE_TEMPLATE/release.md | 16 +++++++-- Makefile | 24 ++++++++++++- concrete/__init__.py | 1 + concrete/version.py | 4 +++ script/make_utils/set_version.sh | 60 +++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 concrete/version.py create mode 100755 script/make_utils/set_version.sh diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index daa36d476..104c67119 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -10,7 +10,13 @@ Release check-list: If it was not already done: - [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Zrc?` for Release Candidates) following semantic versioning: https://semver.org/ -- [ ] Update the version in pyproject.toml to `X.Y.Z` (or `X.Y.Zrc?`) +- [ ] Update the project version to `X.Y.Z` (or `X.Y.Zrc?`) by running: + +```bash +VERSION=X.Y.Z make set_version +# or +VERSION=X.Y.Zrc? make set_version +``` Then: - [ ] For non RC releases: check the release milestone issues, cut out what can't be completed in time and change the milestones for these issues @@ -26,6 +32,12 @@ This is the release markdown template you should copy and update: To continue the release cycle: - [ ] Choose the version number for next release, e.g. `vA.B.C` (can be `vA.B.Crc?` for Release Candidates) following semantic versioning: https://semver.org/ -- [ ] Update the version in pyproject.toml to `A.B.C` (or `A.B.Crc?`) +- [ ] Update the project version to `A.B.C` (or `A.B.Crc?`) by running: + +```bash +VERSION=A.B.C make set_version +# or +VERSION=A.B.Crc? make set_version +``` All done! diff --git a/Makefile b/Makefile index 6569e25c7..478c5ad39 100644 --- a/Makefile +++ b/Makefile @@ -71,7 +71,9 @@ pcc: --no-print-directory pcc_internal .PHONY: pcc -pcc_internal: check_python_format check_finalize_nb python_linting mypy_ci pydocstyle shell_lint +PCC_DEPS := check_python_format check_finalize_nb python_linting mypy_ci pydocstyle shell_lint +PCC_DEPS += check_version_coherence +pcc_internal: $(PCC_DEPS) .PHONY: pcc_internal pytest: @@ -214,3 +216,23 @@ shell_lint: find \( -path "./.venv" -o -path "./.docker_venv" \) -prune -o -type f -name "*.sh" -print | \ xargs shellcheck .PHONY: shell_lint + +set_version: + @if [[ "$$VERSION" == "" ]]; then \ + echo "VERSION env variable is empty. Please set to desired version."; \ + exit 1; \ + fi; + ./script/make_utils/set_version.sh --version "$${VERSION}" --src-dir "$(SRC_DIR)" +.PHONY: set_version + +check_version_coherence: + @SRC_VER=$$(poetry run python -c "from $(SRC_DIR) import __version__; print(__version__);");\ + PROJECT_VER=($$(poetry version)); \ + PROJECT_VER="$${PROJECT_VER[1]}"; \ + echo "Source version: $${SRC_VER}"; \ + echo "Project version: $${PROJECT_VER}"; \ + if [[ "$${SRC_VER}" != "$${PROJECT_VER}" ]]; then \ + echo "Version mismatch between source and pyproject.toml re-run make set_version"; \ + exit 1; \ + fi +.PHONY: check_version_coherence diff --git a/concrete/__init__.py b/concrete/__init__.py index dd0d3d4c0..5200aadd1 100644 --- a/concrete/__init__.py +++ b/concrete/__init__.py @@ -1,2 +1,3 @@ """Package top import.""" from . import common, numpy +from .version import __version__ diff --git a/concrete/version.py b/concrete/version.py new file mode 100644 index 000000000..c194a9e64 --- /dev/null +++ b/concrete/version.py @@ -0,0 +1,4 @@ +"""Package version module.""" +# Auto-generated by "make set_version" do not modify + +__version__ = "0.2.0rc1" diff --git a/script/make_utils/set_version.sh b/script/make_utils/set_version.sh new file mode 100755 index 000000000..0c75efa91 --- /dev/null +++ b/script/make_utils/set_version.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +VERSION_TO_SET= +SRC_DIR= + +while [ -n "$1" ] +do + case "$1" in + "--version" ) + shift + VERSION_TO_SET="$1" + ;; + + "--src-dir" ) + shift + SRC_DIR="$1" + ;; + + *) + echo "Unknown param : $1" + exit 1 + ;; + esac + shift +done + +if [[ "${VERSION_TO_SET}" == "" ]]; then + echo "--version is required. Aborting" + exit 1 +fi + +if [[ "${SRC_DIR}" == "" ]]; then + echo "--src-dir is required. Aborting" + exit 1 +fi + +rx='^(v)?([0-9]+\.){2}[0-9]+(rc[0-9]+)?$' + +if [[ ! "${VERSION_TO_SET}" =~ $rx ]]; then + echo "ERROR: Unable to validate version: '${VERSION_TO_SET}'" + exit 1 +fi + +echo "INFO: Version ${VERSION_TO_SET}" + +VERSION_TO_SET="${VERSION_TO_SET/v/}" +echo "${VERSION_TO_SET}" + +poetry version "${VERSION_TO_SET}" + +VERSION_FILE="${SRC_DIR}/version.py" + +rm "${VERSION_FILE}" + +{ + echo '"""Package version module."""' + echo '# Auto-generated by "make set_version" do not modify' + echo '' + echo "__version__ = \"${VERSION_TO_SET}\"" +} >> "${VERSION_FILE}" From 003bad581a79c4337324b1ea22ecf4e4aafbe777 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 4 Oct 2021 12:05:20 +0300 Subject: [PATCH 0358/1104] feat(fhe_circuit): create FHECircuit class to combine operation graph and compiler engine --- concrete/common/fhe_circuit.py | 57 +++++++++ concrete/numpy/compile.py | 7 +- docs/dev/explanation/COMPILATION.md | 4 +- .../explanation/TERMINOLOGY_AND_STRUCTURE.md | 4 + .../QuantizedLinearRegression.ipynb | 59 ++++----- .../QuantizedLogisticRegression.ipynb | 113 +++++++++--------- docs/user/howto/COMPILING_AND_EXECUTING.md | 12 +- docs/user/tutorial/ARITHMETIC_OPERATIONS.md | 36 +++--- docs/user/tutorial/TABLE_LOOKUP.md | 24 ++-- .../tutorial/WORKING_WITH_FLOATING_POINTS.md | 10 +- tests/common/test_fhe_circuit.py | 50 ++++++++ 11 files changed, 239 insertions(+), 137 deletions(-) create mode 100644 concrete/common/fhe_circuit.py create mode 100644 tests/common/test_fhe_circuit.py diff --git a/concrete/common/fhe_circuit.py b/concrete/common/fhe_circuit.py new file mode 100644 index 000000000..03b6b3623 --- /dev/null +++ b/concrete/common/fhe_circuit.py @@ -0,0 +1,57 @@ +"""Module to hold the result of compilation.""" + +from pathlib import Path +from typing import List, Optional, Union + +from zamalang import CompilerEngine + +from .debugging import draw_graph, get_printable_graph +from .operator_graph import OPGraph + + +class FHECircuit: + """Class which is the result of compilation.""" + + opgraph: OPGraph + engine: CompilerEngine + + def __init__(self, opgraph: OPGraph, engine: CompilerEngine): + self.opgraph = opgraph + self.engine = engine + + def __str__(self): + return get_printable_graph(self.opgraph, show_data_types=True) + + def draw( + self, + show: bool = False, + vertical: bool = True, + save_to: Optional[Path] = None, + ) -> str: + """Draw operation graph of the circuit and optionally save/show the drawing. + + Args: + show (bool): if set to True, the drawing will be shown using matplotlib + vertical (bool): if set to True, the orientation will be vertical + save_to (Optional[Path]): if specified, the drawn graph will be saved to this path; + otherwise it will be saved to a temporary file + + Returns: + str: path of the file where the drawn graph is saved + + """ + + return draw_graph(self.opgraph, show, vertical, save_to) + + def run(self, *args: List[Union[int, List[int]]]) -> int: + """Encrypt, evaluate, and decrypt the inputs on the circuit. + + Args: + *args (List[Union[int, List[int]]]): inputs to the circuit + + Returns: + int: homomorphic evaluation result + + """ + + return self.engine.run(*args) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 8f7790a90..a5d499996 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -11,6 +11,7 @@ from ..common.bounds_measurement.inputset_eval import eval_op_graph_bounds_on_in from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.data_types import Integer +from ..common.fhe_circuit import FHECircuit from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( extend_direct_lookup_tables, @@ -237,7 +238,7 @@ def _compile_numpy_function_internal( compilation_configuration: CompilationConfiguration, compilation_artifacts: CompilationArtifacts, show_mlir: bool, -) -> CompilerEngine: +) -> FHECircuit: """Compile an homomorphic program (internal part of the API). Args: @@ -282,7 +283,7 @@ def _compile_numpy_function_internal( engine = CompilerEngine() engine.compile_fhe(mlir_result) - return engine + return FHECircuit(op_graph, engine) def compile_numpy_function( @@ -292,7 +293,7 @@ def compile_numpy_function( compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, show_mlir: bool = False, -) -> CompilerEngine: +) -> FHECircuit: """Compile an homomorphic program (main API). Args: diff --git a/docs/dev/explanation/COMPILATION.md b/docs/dev/explanation/COMPILATION.md index 5e5f7e393..0dd09ab7d 100644 --- a/docs/dev/explanation/COMPILATION.md +++ b/docs/dev/explanation/COMPILATION.md @@ -22,13 +22,13 @@ x = hnp.EncryptedScalar(hnp.UnsignedInteger(2)) y = hnp.EncryptedScalar(hnp.UnsignedInteger(1)) # Compile the function to its homomorphic equivalent -engine = hnp.compile_numpy_function( +circuit = hnp.compile_numpy_function( f, {"x": x, "y": y}, [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)], ) # Make homomorphic inference -engine.run(1, 0) +circuit.run(1, 0) ``` ## Overview diff --git a/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md index 706986fef..f1934029c 100644 --- a/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md +++ b/docs/dev/explanation/TERMINOLOGY_AND_STRUCTURE.md @@ -12,6 +12,10 @@ In this section we will go over some terms that we use throughout the project. - bounds - before intermediate representation is sent to the compiler, we need to know which node will output which type (e.g., uint3 vs uint5) - there are several ways to do this but the simplest one is to evaluate the intermediate representation with all combinations of inputs and remember the maximum and the minimum values for each node, which is what we call bounds, and bounds can be used to determine the appropriate type for each node +- fhe circuit + - it is the result of compilation + - it contains the operation graph and the compiler engine in it + - it has methods for printing, visualizing, and evaluating ## Module structure diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index 61d59c9be..db1b74634 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -201,7 +201,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAD4CAYAAAAXUaZHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAhT0lEQVR4nO3deXxU1fnH8c+jWCqKxgUtgopWrbKDUUFFrbjhbheq7a+igoggDVErahej1Yq4xGgRBUHBlSIIiKyySmUL+yayCAoqoLIoKBJyfn+cOzoJCUlIJvdm5vt+vfLKnTN3ksd5jQ9PnnvuOeacQ0REkst+YQcgIiIVT8ldRCQJKbmLiCQhJXcRkSSk5C4ikoSqhR0AwJFHHunq1asXdhgiIlXKnDlzvnTO1SrquUgk93r16pGbmxt2GCIiVYqZrS3uObVlRESSkJK7iEgSUnIXEUlCSu4iIklIyV1EJAkpuYuIJCEldxGRJKTkLiISgh07oHt3WFvsTPXyUXIXEalkEydCo0bQsyeMGpWY36HkLiJSSbZsgVtvhdatYb/9YPJkuP32xPwuJXcRkUowYgQ0aAD9+8Pdd8OCBXD++Yn7fUruIiIJtHEjXH89XHMNHHEEzJwJjz8ONWok9vcquYuIJIBz8OqrcNppMHQoPPQQ5OZCenrl/P5IrAopIpJMPv0UOnXyF0tbtIB+/aB+/cqNQZW7iEgFyc+H3r19b33yZMjOhmnTKj+xgyp3EZEKsWIFdOgAU6f62TB9+sCJJ4YXjyp3EZFyyMvz89UbN/YzYPr1g/Hjw03soMpdRGSfLVgA7dvDnDlw7bXQqxccc0zYUXmq3EVEymjnTvjHP/zMl08/hcGD/YyYqCR2UOUuIlIm06f7an3ZMrjxRnjqKT9/PWpUuYuIlMK330K3bnDOObB9O4weDQMGRDOxgyp3EZESjR8PHTvCmjXQpbPj0R5GzZrBk86BWZjhFUmVu4hIMTZv9i2YSy6B6tXh/Zv7858DMql5sPMnOAeZmZCVFWqcRVFyFxEpwrBh/uajAQPgvvtg/jzHuYcshJwcn9BjiT0nxy/36FzYIRegtoyISJwNG6BrVz8DpkkTePddaN4cwPwtp+ATek6OP87I8OMRa82ochcRwRfeAwf6hb5GjIBHHoHZs2OJPWBxCT4mgokdlNxFRFi7Ftq0gXbtfHKfPx/uvx8OOKDQibFWTLxYiyZilNxFJGXl5/u7Shs29At8PfssvP8+nHpqESfH99gzMvyLMzIK9uAjRD13EUlJy5f7hb6mTfOzYfr0geOP38sLzCAtrWCPPdaiSUuLXGvGXAT+tUlPT3e5ublhhyEiVVXhueZ7mXu+axc88QQ8+KDfDSk7299pWurcXIbflWhmNsc5V+T2H6Vqy5jZGjNbZGbzzSw3GDvczMab2Yrg+2HBuJnZM2a20swWmlnzvf90EZFyyMoq2BbZy9zzefPgrLN8P/3KK2HpUt9nL1NuLnxyxCr2mLL03H/tnGsa96/EvcAE59zJwITgMUAb4OTgqyPQu6KCFREpwDk/x7yEuefff+8T+hlnwGefwZAh8NZb8ItfhBp9QpWn534NcEFwPACYDHQPxgc63++ZYWZpZlbbOfd5eQIVEdlDfN+7mLnn06b53vry5XDTTX6hr8MOCy3iSlPayt0B48xsjpl1DMaOjkvYXwBHB8d1gE/jXrsuGCvAzDqaWa6Z5W7atGkfQhcRodi55998a3TtCued5yv3cePgpZdSI7FD6ZP7uc655viWSxczOy/+yaBKL9OVWedcH+dcunMuvVatWmV5qYjIT4qYez72Ny/QsKGjVy9/t+nixXDxxSHFF5JSJXfn3Prg+0bgbeBMYIOZ1QYIvm8MTl8PHBv38rrBmIhIxSo09/zrL/O56bSZXDasEzW+2cj7Ux05OXDwwWEHWvlKTO5mdpCZ1YwdA5cAi4ERQLvgtHbA8OB4BHBjMGumBbBV/XYRSYi4uedvnZPNafWNVz86g7+dMY55nftyzrnRnMlSGUpzQfVo4G3z032qAa8758aY2Wzgv2bWHlgLtA3OHwVcDqwEdgA3V3jUIiKBLzpl0aWLY2hbo1kzGDvWaNrkYrBLwg4tVCUmd+fcaqBJEeNfAa2LGHdAlwqJTkSkGM755XgzM+G774wePeCuu6BaNYDUrdhjtPyAiFQ5a9b4nZHGj4dWreDFF+GUU8KOKlq0cJiIVBm7d8Mzz/iFvqZP94t+TZ6sxF4UVe4iUiUsW+ZvRvrgA7887/PPw3HHhR1VdKlyF5FI27XLb5zRtCl8+CG88orfHUmJfe9UuYtIZM2ZA7fcAgsXQtu2fr31o44KO6qqQZW7iETOd9/Bvff6FRw3bfKbVQ8apMReFqrcRSRSpk71vfUVK/z3xx/39ylJ2ahyF5FI2LYNOneG88+HvDx47z3o21eJfV8puYtI6EaN8tMbn38eunWDRYug9R63SEpZqC0jIqH56iufzF99FerX99McW7QIO6rkoMpdRCqdc/Df/8Jpp8Gbb8I//gFz5yqxVyRV7iJSqT77zPfWhw+H9HTfW2/cOOyoko8qdxGpFM5Bv36+/TJ2LPTs6ZcQUGJPDFXuIpJwq1f7hb4mTPCzYV58EU46KeyokpsqdxFJmN274emnoVEjmDXLz4aZOFGJvTKocheRhFiyBNq3h5kz4YorfGKvWzfsqFKHKncRqVA//AAPPQTNmsGqVfD66/DOO0rslU2Vu4hUmNmzfbW+aBFcf71fe71WrbCjSk2q3EWk3HbsgL/+1c9T//prGDEC3nhDiT1MqtxFpFwmT/YLfK1aBbfe6hf6OvTQsKMSVe4isk+2boXbboNf/9o/njgR+vRRYo8KJXcRKbORI6FBAz9f/a67/GYasSQv0aDkLiKltmkT/PGPcNVVcNhh/g7TJ56AGjXCjkwKU3IXkRI55y+Q1q8Pb70FWVl+C7wzzww7MimOLqiKyF6tW+cX+nrnHZ/M+/Xza69LtKlyF5GCnAMgP99fIG3QwPHee/Dkk369dSX2qkHJXUR+kpUFmZmsXOFo3drPhjn9kBUs6pDDnXfC/vuHHaCUlpK7iHjOkff1Np7IqUaj03Yxd66jT+s3mbDuV/xyv49/rOilalDPXUQAWLTYaD/jSWZjXL17OM9t60ydCZ9BRgZkZ4NZ2CFKGahyF0lxO3fCAw9A8+awZo3x5huOYVxLHT7zJyixV0lK7iIpbOZMOP10v4rj9dfD0iWOP8zIpEAqz8xUS6YKUnIXSUHbt8Odd0LLln4ZgXffhVcGOo58JBNycnwrJj/ff8/JUYKvgtRzF0kxEyb4Bb4+/tjPX3/0UTjkEACDtLSCPfbsbP+itDS1ZqoYJXeRFLFlC9x9t78J6eSTYcoUOO+8QidlZfkKPZbIYwleib3KUVtGpCoo3BIpY4tk+HC/dMDLL0P37rBgQRGJPaZwIldir5JKndzNbH8zm2dmI4PHJ5jZTDNbaWaDzOxnwXj14PHK4Pl6CYpdJDUENxb9mNCd84+zskp86caN8Ic/wLXX+o0zZs6EHj3gwAMTGbBEQVkq9wxgWdzjx4Bs59xJwGagfTDeHtgcjGcH54nIvnDO91PiL2pmBhc9t2wptoJ3Dl57zVfrw4bBww9Dbq6fGSMpwjlX4hdQF5gAXAiMBAz4EqgWPN8SGBscjwVaBsfVgvNsbz//9NNPdyJSjPx85zIynPM5239lZPjxInzyiXOXX+5Pa9HCuSVLKjVaqURArismr5a2cn8auAfIDx4fAWxxzuUFj9cBdYLjOsCnwT8cecDW4PwCzKyjmeWaWe6mTZtKGYZICoqftRJTxEXO/Hzo3dtvojF5si/up03z1buknhKTu5ldCWx0zs2pyF/snOvjnEt3zqXX0i66IsWLtWLiFZp3/tFHcMEFfmrjWWfB4sXwl79ooa9UVprK/RzgajNbA7yJb83kAGlmFptKWRdYHxyvB44FCJ4/FPiqAmMWSR3xPfYibizK2+Xo2ROaNIFFi6B/fxg3Dk44IezAJWwlznN3zt0H3AdgZhcAdzvn/mRmg4Hf4RN+O2B48JIRwePpwfMTg96QiJSVFX9j0YLvf8UtLYy5c+G666BXL6hdO9xwJTrKcxNTd+BNM3sYmAf0C8b7Aa+Y2Urga+D68oUokuIK3Vi08wfj4YOz6dHLOOIIv+3db38bbogSPWVK7s65ycDk4Hg1sMcOis6574HfV0BsIhITJPYPPoD27eHDD40bb/RF/OGHhxybRJLuUBWpAr791ndmzj0XduyAMWNgwAAldime1pYRibhx46BjR/jkk58W+qpZM+yoJOpUuYtE1ObNcMstcOml8POfw9Sp8J//KLFL6Si5i0TQ0KH+5qOBA+G++2D+fN+SESkttWVEIuSLL+COO2DIEGjaFEaNgmbNwo5KqiJV7iIR4Jy/QFq/PowcCY88ArNmKbHLvlPlLhKytWuhUyc/A+bss/1mGqeeGnZUUtWpchcJSX6+v6u0YUN4/3145hn/XYldKoIqd5EQLF8OHTr4VRsvvRReeAGOPz7sqCSZqHIXqUS7dvl56k2awJIlftu70aOV2KXiqXIXqSTz5vmlA+bNg9/9Dp59Fn7xi7CjkmSlyl0kwb7/Hu6/H844Az7/3E9zHDxYiV0SS5W7SAJNm+Z768uXw803w5NPwmGHhR2VpAJV7iIJ8M03/makVq185T5unN9IQ4ldKouSu0gFGzvWT2987jm/kuPixXDxxWFHJalGyV2kgnz1FbRrB5ddBjVq+JbM00/DwQeHHZmkIiV3kXJyzu+GVL8+vP46/P3vfqGvs88OOzJJZbqgKlIOn38OXbrA229D8+a+t96kSdhRiahyF9knzsFLL/lqffRoeOwxmDlTiV2iQ5W7SBmtWeN3Rho/3s+GefFFOOWUsKMSKUiVu0gp7d7tF/dq2BCmT/ezYSZPVmKXaFLlLlIKy5b5pQOmT4c2beD55+G448KOSqR4qtxF9mLXLr9xRtOm/i7TV16Bd98NErtzBU8u/FgkREruIsWYMwfS0/3Uxmuv9dX7//0fmAFZWZCZ+VNCd84/zsoKL2CROEruIoV89x107w5nnQWbNvlpjoMGwVFHBSc4B1u2QE7OTwk+M9M/3rJFFbxEgnruInGmTIFbb4UVK3yP/YknIC2t0ElmkJ3tj3Ny/Bf4tQays4PSXiRcqtxFgG3b4Pbb4YILIC8P3nvPT3HcI7HHxCf4GCV2iRAld0l5o0ZBgwZ+q7tu3WDRImjduoQXxVox8eJ78CIhU3KXlPXll/4C6RVXwCGH+GmO2dlw0EElvDC+x56R4Xe6zsgo2IMXCZl67pJynPMXSLt29dc///lPv1NS9eql/AFmvl8T32OPtWjS0tSakUgwF4EqIz093eXm5oYdhqSA9euhc2cYMcJPc+zfHxo12scf5lzBRF74sUiCmdkc51x6Uc+pLSMpwTno29cv9DVuHPTs6dsw+5zYYc9ErsQuEaK2jCS91av99MaJE+H88/0smJNOCjsqkcRS5S5Ja/du3wpv2BByc/16MBMnKrFLaigxuZvZz81slpktMLMlZvZgMH6Cmc00s5VmNsjMfhaMVw8erwyer5fg/waRPSxeDOecA3fe6ac1LlkCt90G+6mckRRRmo/6TuBC51wToClwmZm1AB4Dsp1zJwGbgfbB+e2BzcF4dnCeSKX44Qd48EG/K9KqVX7buxEjoG7dsCMTqVwlJnfnfRs8PCD4csCFwFvB+ADg2uD4muAxwfOtzXSlSSrIXlZinD0bTj/dr931+9/D0qVwww26zimpqVQXVM1sf2AOcBLQC1gFbHHO5QWnrAPqBMd1gE8BnHN5ZrYVOAL4stDP7Ah0BDhOC2NLaWRl+Ynpsbnlwc1EOw6qxQM//I2nnoLatX2lftVVYQcrEq5SJXfn3G6gqZmlAW8Dp5b3Fzvn+gB9wM9zL+/PkyQXvxIj+ASfmcnknPnceuhgVm71PfXHHoNDDw01UpFIKNNUSOfcFjObBLQE0sysWlC91wXWB6etB44F1plZNeBQ4KsKjFlSUaGVGLfmvMQ99KQPT/PLIx2ThvlFv0TEK81smVpBxY6ZHQhcDCwDJgG/C05rBwwPjkcEjwmen+iicBusVH1Bgh/JFTRgCS/SgbvvcixcaErsIoWUZrZMbWCSmS0EZgPjnXMjge7AnWa2Et9T7xec3w84Ihi/E7i34sOWVLRpo+OPp87lKkZyGJuZTksez8ukxoGqHUQKK7Et45xbCDQrYnw1cGYR498Dv6+Q6ETw7fY3Xnf8pcN2tn3fiIdajKL75Db8rPvZBXvwmhYj8iMtPyCRtm6d30Rj5EjjrDpb6PfrV2kwsLtWYhQpgZK7RFJ+vl8D5q9/9TsjZWdD16512X+/7j8l8liCV2IX2YOSu0TOypV+oa/Jk+HCC/1qjieeGHtWKzGKlIZW2pDIyMvzG1I3agTz5vnK/b334hO7iJSWKnepHCVsbLFwIbRv71dvvPpq6N0bjjkmhDhFkoQqd0m8rKyCe4vG9iDNymLnTnjgAb8mzNq1fvu7YcOU2EXKS8ldEit+2YBYgg82l56x7FCaN3c89JBf4GvZMmjbVm10kYqgtowkVqFlA8jJYTs1+EezSTw9+Hzq1jVGjYI2bcINUyTZqHKXxItL8BO4kEYsInveBXTqZCxerMQukghK7pJ4zrGl8/10oC8XMYFq5DHld8/wXC/HIYeEHZxIclJbRhLLOYZd3Z/OI7uywX7BPXc7snb05cBeT0Dmat2EJJIgSu6SMBs2QNeuxuCR7Wl85HpGjDLSzzBwPaHaLi0bIJJASu5S4ZyD116DjAz49lt4+GG456/HcMDPtGyASGVRcpcK9ckn0KkTjB4NLVv6u0zr1wctGyBSuXRBVSpEfj489xw0aABTp8Izz8D778cSu4hUNlXuUm4ffQQdOvhkftFFfqGvevXCjkoktalyl32Wl+c3pG7cGBYtgpdegnHjlNhFokCVu+yTBQvglltg7ly47jro1Qtq1w47KhGJUeUuZfL99/D3v0N6ut8lafBgGDpUiV0kalS5S6l98IHvrS9bBu3awVNPweGHhx2ViBRFlbuU6Ntv/Zz1c8+F7dthzBh4+WUldpEoU+UuezV+PHTs6Nda79IF/v1vqFkz7KhEpCSq3KVImzfDzTfDJZdA9ep+7vqzzyqxi1QVSu6yh6FD/c1Hr7wC990H8+f7loyIVB1qy8iPvvgC7rgDhgyBpk3h3XehefOwoxKRfaHKPVnF9ist7nGhpwYM8NX6yJHw6KMwa5YSu0hVpuSejPayIXVha9f6nZBuusmvC7NgAdx7LxxwQGUGLCIVTck92exlQ2q2bPkx4efnw3/+4xP6//7nj6dMgV/9KtToRaSCqOeebIrYkBrwE9WDNdSXL4f27X1Sv/RSeOEFOP748EIWkYqnyj0ZxSf4mOxsduUZjz4KTZrA0qW+zz56tBK7SDJSck9GsVZMnLl/epIzz3Tcfz9cdZVfQuDGG7VnhkiyUnJPNvE99owMvtuez33p4znzjW588dE2hrzlGDwYjj467EBFJJGU3JONmd94OiODab/Npmkzo0fuRbSrn8vSO3rzm9+qVBdJBbqgmoS+uSuL++519DrPqFfPrw9zUeuzwFqEHZqIVBJV7klmzBho2BCe62385S9+h6SLLkLNdZEUU2JyN7NjzWySmS01syVmlhGMH25m481sRfD9sGDczOwZM1tpZgvNTPc5VoKvvvJrrLdpAwcd5Kc55uTAwQeHHZmIhKE0lXsecJdzrj7QAuhiZvWBe4EJzrmTgQnBY4A2wMnBV0egd4VHLT9yzu+GVL8+vP663yVp3jxo2TLsyEQkTCUmd+fc5865ucHxN8AyoA5wDTAgOG0AcG1wfA0w0HkzgDQz0yZsCfDZZ/Cb30DbtnDssZCbC//6l1+iV0RSW5l67mZWD2gGzASOds59Hjz1BRCbXFcH+DTuZeuCscI/q6OZ5ZpZ7qZNm8oad0pzDvr189X6mDHQowfMmOFvThIRgTIkdzM7GBgCdHPObYt/zjnngOKXHSyCc66Pcy7dOZdeq1atsrw0pX38sd9Ao0MHaNzYL/TVvTtU07wnEYlTquRuZgfgE/trzrmhwfCGWLsl+L4xGF8PHBv38rrBmJTD7t3+AmnDhr5K79ULJk+GU04JOzIRiaLSzJYxoB+wzDn3VNxTI4B2wXE7YHjc+I3BrJkWwNa49o3sg6VLoVUr6NYNzj/fP+7cGfbTRFYRKUZp/pg/B/gzsMjM5gdj9wM9gP+aWXtgLdA2eG4UcDmwEtgB3FyRAaeSH36Anj39RdKaNf22d3/6k6asi0jJSkzuzrlpQHHppHUR5zugSznjSnm5uX5Z3oUL4frrfUvmqKPCjkpEqgr9YR8x330H99wDZ50FX34Jw4fDG28osYtI2WiORYRMmQK33gorVvjvPXv6NcBERMpKlXsEbNsGt98OF1zgZ8VMmAB9+iixi8i+U3IP2ahRfh/TPn3gzjv9Ql8XXhh2VCJS1Sm5h+TLL+HPf4YrroBDD4UPPoAnn4QaNcKOTESSgZJ7JXMOBg3ySwcMGgQPPABz5/oLqCIiFUUXVCvR+vX+5qMRI+CMM/z6MI0ahR2ViCQjVe6VwDno29dX6+PGwRNPwPTpSuwikjiq3BNs1So/rXHSJD8bpm9fOOmksKMSkWSnyj1Bdu+Gp57y1fmcOX42zMSJSuwiUjlUuSfA4sV+6YBZs+Cqq6B3b6izx4r2IiKJo8q9Av3wAzz4IDRvDqtX+23vhg9XYheRyqfKvYLMmuWr9cWL4YYb/EJf2oNERMKiyr2cduyAu+/2G1Jv3gzvvOMrdiV2EQmTKvdymDTJb3e3ejXcdhs89pi/21REJGyq3PfB1q0+mV94od84Y9IkeP55JXYRiQ4l9zJ65x1/M9KLL/p2zMKFfv66iEiUKLmX0qZN/kLp1VfDEUfAzJnw+ONa6EtEoknJvQTO+Qukp50GQ4b4qY65uZCeHnZkIiLF0wXVvfj0U7+Jxrvv+lUb+/Xza6+LiESdKvci5OfDCy/4RD5pEmRnw//+p8QuIlWHKvdCYvuXTpkCrVv7NWFOPDHsqEREykaVeyAvzy/F27gxzJ/vV28cP16JXUSqJlXu+OmM7dv7C6XXXAPPPQfHHBN2VCIi+y6lK/edO+Gf/4TTT4e1a+HNN+Htt5XYRaTqS9nKfcYMX60vXeo3qs7O9vPXcQ6wsMMTESmXlKvct2+HzEw4+2z4Zt1WRl3zAgMHuJ8Se2YmZGWFHaaISLmkVHKfMMHvjPT009DpNsfiP/6bNsM7+YQeS+w5ObBlS1DBi4hUTSnRltmyxa8D068fnHwyTJ0KrVoZuB5QfadP6Dk5/uSMDN+jMbVmRKTqSvrKfdgwv9DXyy9D9+6wYAG0ahU8aeYTeTwldhFJAkmb3DdsgLZt4brr4Kij/EJfPXrAgQfGnRRrxcSLtWhERKqwpEvuzsHAgX6hr+HD4ZFHYPZsP91xjxNjPfaMDL/mQEaGf6wELyJVXFL13D/5xG+iMWaMnw3z4os+yRfJDNLSCvbYYy2atDS1ZkSkSjMXgQo1PT3d5ebm7vPr8/P9Tkjdu/uC+9FHoUsX2K80f5c4VzCRF34sIhJRZjbHOVfkAuQlpj8z629mG81scdzY4WY23sxWBN8PC8bNzJ4xs5VmttDMmlfcf0bRli+H88/3ybxlS1i8GLp2LWVihz0TuRK7iCSB0qTAl4HLCo3dC0xwzp0MTAgeA7QBTg6+OgK9KybMovXvD02a+IT+0kswdizUq5fI3ygiUjWUmNydc1OBrwsNXwMMCI4HANfGjQ903gwgzcxqV1CsezjlFLjySli2DG66SUW3iEjMvl5QPdo593lw/AVwdHBcB/g07rx1wdjnFGJmHfHVPccdd9w+BXHuuf5LREQKKvdUSOevyJb5qqxzro9zLt05l16rVq3yhiEiInH2NblviLVbgu8bg/H1wLFx59UNxkREpBLta3IfAbQLjtsBw+PGbwxmzbQAtsa1b0REpJKU2HM3szeAC4AjzWwd8ADQA/ivmbUH1gJtg9NHAZcDK4EdwM0JiFlEREpQYnJ3zt1QzFOtizjXAV3KG5SIiJRP0q0tIyIiSu4iIklJyV1EJAlFYuEwM9uEvzAbpiOBL0OOoawUc+JVtXhBMVeWKMR8vHOuyBuFIpHco8DMcotbXS2qFHPiVbV4QTFXlqjHrLaMiEgSUnIXEUlCSu4/6RN2APtAMSdeVYsXFHNliXTM6rmLiCQhVe4iIklIyV1EJAmlbHI3szVmtsjM5ptZbjBW5N6wYTOzXwVxxr62mVk3M8sys/Vx45eHHGek99stQ8yPm9mHQVxvm1laMF7PzL6Le7+fj1DMxX4WzOy+4H1ebmaXRijmQXHxrjGz+cF46O+zmR1rZpPMbKmZLTGzjGA80p/nApxzKfkFrAGOLDTWE7g3OL4XeCzsOIuIe3/87lfHA1nA3WHHFBfbeUBzYHFJ7yl+9dDRgAEtgJkRivkSoFpw/FhczPXiz4vY+1zkZwGoDywAqgMnAKuA/aMQc6HnnwT+GZX3GagNNA+OawIfBe9lpD/P8V8pW7kXo7i9YaOkNbDKORf2Hb17cBHeb7c4RcXsnBvnnMsLHs7AbzoTGcW8z8W5BnjTObfTOfcxfjnuMxMWXDH2FrOZGX7Z8DcqNai9cM597pybGxx/AyzDbxka6c9zvFRO7g4YZ2Zzgv1cofi9YaPkegr+T3BH8Gdg/6i0kQop6367UXMLviKLOcHM5pnZFDNrFVZQxSjqs1AV3udWwAbn3Iq4sci8z2ZWD2gGzKQKfZ5TObmf65xrDrQBupjZefFPOv+3VqTmiZrZz4CrgcHBUG/gl0BT/CbkT4YTWelE8T3dGzP7G5AHvBYMfQ4c55xrBtwJvG5mh4QVXyFV6rNQyA0ULFgi8z6b2cHAEKCbc25b/HNR/zynbHJ3zq0Pvm8E3sb/qVrc3rBR0QaY65zbAOCc2+Cc2+2cywf6EsKf26VQJffbNbObgCuBPwX/ExO0Nr4Kjufg+9enhBZknL18FqL+PlcDfgMMio1F5X02swPwif0159zQYLjKfJ5TMrmb2UFmVjN2jL+Atpji94aNigIVTqGe3nX4/4aoqXL77ZrZZcA9wNXOuR1x47XMbP/g+ETgZGB1OFEWtJfPwgjgejOrbmYn4GOeVdnx7cVFwIfOuXWxgSi8z8F1gH7AMufcU3FPVZ3Pc9hXdMP4Ak7EzyBYACwB/haMHwFMAFYA7wGHhx1rXMwHAV8Bh8aNvQIsAhbiP1y1Q47xDfyf1LvwPcf2xb2n+FkFvfBV2SIgPUIxr8T3T+cHX88H5/42+LzMB+YCV0Uo5mI/C8Dfgvd5OdAmKjEH4y8DnQqdG/r7DJyLb7ksjPscXB71z3P8l5YfEBFJQinZlhERSXZK7iIiSUjJXUQkCSm5i4gkISV3EZEkpOQuIpKElNxFRJLQ/wMtV4lCm6lYAwAAAABJRU5ErkJggg==\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -234,7 +234,7 @@ "output_type": "stream", "text": [ "[[2.669915]]\n", - "-3.2335129\n" + "-3.2335143\n" ] } ], @@ -517,7 +517,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -630,7 +630,7 @@ "id": "01d67c28", "metadata": {}, "source": [ - "### Let's compile our quantized inference function to it's operation graph for visualization" + "### Let's compile our quantized inference function to it's homomorphic equivalent" ] }, { @@ -644,7 +644,7 @@ "for x_i in x_q:\n", " inputset.append((int(x_i[0]),))\n", "\n", - "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", + "circuit = hnp.compile_numpy_function(\n", " infer,\n", " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", " inputset,\n", @@ -656,7 +656,7 @@ "id": "c62af039", "metadata": {}, "source": [ - "### Here are some representations of the operation graph" + "### Here are some representations of the fhe circuit" ] }, { @@ -681,7 +681,7 @@ } ], "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + "print(circuit)" ] }, { @@ -694,7 +694,7 @@ "data": { "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -703,33 +703,11 @@ ], "source": [ "from PIL import Image\n", - "file = Image.open(hnp.draw_graph(homomorphic_model))\n", + "file = Image.open(circuit.draw())\n", "file.show()\n", "file.close()" ] }, - { - "cell_type": "markdown", - "id": "de19a433", - "metadata": {}, - "source": [ - "### It's time to compile the function to its homomorphic equivalent" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "cf89c63d", - "metadata": {}, - "outputs": [], - "source": [ - "engine = hnp.compile_numpy_function(\n", - " infer,\n", - " {\"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False))},\n", - " inputset,\n", - ")" - ] - }, { "cell_type": "markdown", "id": "46753da7", @@ -740,14 +718,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "c0b246f7", "metadata": {}, "outputs": [], "source": [ "homomorphic_predictions = []\n", "for x_i in map(lambda x_i: int(x_i[0]), x_q):\n", - " inference = QuantizedArray(engine.run(x_i), y_q.parameters)\n", + " inference = QuantizedArray(circuit.run(x_i), y_q.parameters)\n", " homomorphic_predictions.append(inference.dequantize())\n", "homomorphic_predictions = np.array(homomorphic_predictions, dtype=np.float32)" ] @@ -762,10 +740,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "92c7f2f5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "ax.plot(inputs, homomorphic_predictions, color=\"green\")\n", "display(fig)" diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index c28845e5b..61fa0b204 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -175,22 +175,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "Epoch: 1 | Loss: 1.0548723936080933\n", - "Epoch: 101 | Loss: 0.13212263584136963\n", - "Epoch: 201 | Loss: 0.07870622724294662\n", - "Epoch: 301 | Loss: 0.055601585656404495\n", - "Epoch: 401 | Loss: 0.04285423085093498\n", - "Epoch: 501 | Loss: 0.03481270745396614\n", - "Epoch: 601 | Loss: 0.029289430007338524\n", - "Epoch: 701 | Loss: 0.025266634300351143\n", - "Epoch: 801 | Loss: 0.02220827341079712\n", - "Epoch: 901 | Loss: 0.019805917516350746\n", - "Epoch: 1001 | Loss: 0.017869682982563972\n", - "Epoch: 1101 | Loss: 0.016276303678750992\n", - "Epoch: 1201 | Loss: 0.014942426234483719\n", - "Epoch: 1301 | Loss: 0.01380960550159216\n", - "Epoch: 1401 | Loss: 0.012835677713155746\n", - "Epoch: 1501 | Loss: 0.011989480815827847\n" + "Epoch: 1 | Loss: 0.9555807113647461\n", + "Epoch: 101 | Loss: 0.12865914404392242\n", + "Epoch: 201 | Loss: 0.0773993730545044\n", + "Epoch: 301 | Loss: 0.05493553355336189\n", + "Epoch: 401 | Loss: 0.042454302310943604\n", + "Epoch: 501 | Loss: 0.034546975046396255\n", + "Epoch: 601 | Loss: 0.02910044603049755\n", + "Epoch: 701 | Loss: 0.02512541227042675\n", + "Epoch: 801 | Loss: 0.02209884487092495\n", + "Epoch: 901 | Loss: 0.019718701019883156\n", + "Epoch: 1001 | Loss: 0.017798559740185738\n", + "Epoch: 1101 | Loss: 0.016217226162552834\n", + "Epoch: 1201 | Loss: 0.014892562292516232\n", + "Epoch: 1301 | Loss: 0.013766967691481113\n", + "Epoch: 1401 | Loss: 0.012798796407878399\n", + "Epoch: 1501 | Loss: 0.011957273818552494\n" ] } ], @@ -253,7 +253,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -291,9 +291,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.53867245]\n", - " [2.37340546]]\n", - "-14.672252655029297\n" + "[[4.54124975]\n", + " [2.37628269]]\n", + "-14.683035850524902\n" ] } ], @@ -734,7 +734,7 @@ "id": "babb1a98", "metadata": {}, "source": [ - "### Let's compile our quantized inference function to it's operation graph for visualization" + "### Let's compile our quantized inference function to its homomorphic equivalent" ] }, { @@ -748,7 +748,7 @@ "for x_i in x_q:\n", " inputset.append((int(x_i[0]), int(x_i[1])))\n", " \n", - "homomorphic_model = hnp.compile_numpy_function_into_op_graph(\n", + "circuit = hnp.compile_numpy_function(\n", " infer,\n", " {\n", " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", @@ -763,7 +763,7 @@ "id": "ab5ba39e", "metadata": {}, "source": [ - "### Here are some representations of the operation graph" + "### Here are some representations of the fhe circuit" ] }, { @@ -794,7 +794,7 @@ } ], "source": [ - "print(hnp.get_printable_graph(homomorphic_model, show_data_types=True))" + "print(circuit)" ] }, { @@ -807,7 +807,7 @@ "data": { "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -816,36 +816,11 @@ ], "source": [ "from PIL import Image\n", - "file = Image.open(hnp.draw_graph(homomorphic_model))\n", + "file = Image.open(circuit.draw())\n", "file.show()\n", "file.close()" ] }, - { - "cell_type": "markdown", - "id": "aac3f0d4", - "metadata": {}, - "source": [ - "### It's time to compile the function to its homomorphic equivalent" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ab75221c", - "metadata": {}, - "outputs": [], - "source": [ - "engine = hnp.compile_numpy_function(\n", - " infer,\n", - " {\n", - " \"x_0\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", - " \"x_1\": hnp.EncryptedScalar(hnp.Integer(input_bits, is_signed=False)),\n", - " },\n", - " inputset,\n", - ")" - ] - }, { "cell_type": "markdown", "id": "972edbb0", @@ -856,17 +831,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "c83f68cd", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5385b79125e44bc493c7eaf5f8013b14", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10000 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "for column in contour.collections:\n", " plt.gca().collections.remove(column)\n", diff --git a/docs/user/howto/COMPILING_AND_EXECUTING.md b/docs/user/howto/COMPILING_AND_EXECUTING.md index 6c5809779..69f2fa819 100644 --- a/docs/user/howto/COMPILING_AND_EXECUTING.md +++ b/docs/user/howto/COMPILING_AND_EXECUTING.md @@ -41,7 +41,7 @@ Finally, we can compile our function to its homomorphic equivalent. ```python -engine = hnp.compile_numpy_function( +circuit = hnp.compile_numpy_function( f, {"x": x, "y": y}, inputset=inputset, ) @@ -49,17 +49,17 @@ engine = hnp.compile_numpy_function( ## Performing homomorphic evaluation -You can use `.run(...)` method of `engine` returned by `hnp.compile_numpy_function(...)` to perform fully homomorphic evaluation. Here are some examples: +You can use `.run(...)` method of `FHECircuit` returned by `hnp.compile_numpy_function(...)` to perform fully homomorphic evaluation. Here are some examples: ```python -engine.run(3, 4) +circuit.run(3, 4) # 7 -engine.run(1, 2) +circuit.run(1, 2) # 3 -engine.run(7, 7) +circuit.run(7, 7) # 14 -engine.run(0, 0) +circuit.run(0, 0) # 0 ``` diff --git a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md index b9ef6d145..d4446deb3 100644 --- a/docs/user/tutorial/ARITHMETIC_OPERATIONS.md +++ b/docs/user/tutorial/ARITHMETIC_OPERATIONS.md @@ -28,8 +28,8 @@ results in ```python -engine.run(3) == 45 -engine.run(0) == 42 +circuit.run(3) == 45 +circuit.run(0) == 42 ``` ### Dynamic ClearScalar and EncryptedScalar @@ -52,8 +52,8 @@ results in ```python -engine.run(6, 4) == 10 -engine.run(1, 1) == 2 +circuit.run(6, 4) == 10 +circuit.run(1, 1) == 2 ``` where @@ -78,8 +78,8 @@ results in ```python -engine.run(7, 7) == 14 -engine.run(3, 4) == 7 +circuit.run(7, 7) == 14 +circuit.run(3, 4) == 7 ``` ## Subtraction @@ -100,8 +100,8 @@ results in ```python -engine.run(2) == 1 -engine.run(3) == 0 +circuit.run(2) == 1 +circuit.run(3) == 0 ``` ### Dynamic ClearScalar and EncryptedScalar @@ -121,8 +121,8 @@ results in ```python -engine.run(2, 4) == 2 -engine.run(1, 7) == 6 +circuit.run(2, 4) == 2 +circuit.run(1, 7) == 6 ``` ## Multiplication @@ -151,8 +151,8 @@ results in ```python -engine.run(2) == 4 -engine.run(5) == 10 +circuit.run(2) == 4 +circuit.run(5) == 10 ``` ### Dynamic ClearScalar and EncryptedScalar @@ -180,8 +180,8 @@ results in ```python -engine.run(2, 3) == 6 -engine.run(1, 7) == 7 +circuit.run(2, 3) == 6 +circuit.run(1, 7) == 7 ``` ## Dot Product @@ -211,8 +211,8 @@ results in ```python -engine.run([1, 1], [2, 3]) == 5 -engine.run([2, 3], [2, 3]) == 13 +circuit.run([1, 1], [2, 3]) == 5 +circuit.run([2, 3], [2, 3]) == 13 ``` ## Combining all together @@ -233,6 +233,6 @@ results in ```python -engine.run([1, 2], [4, 3], 10) == 60 -engine.run([2, 3], [3, 2], 5) == 66 +circuit.run([1, 2], [4, 3], 10) == 60 +circuit.run([2, 3], [3, 2], 5) == 66 ``` diff --git a/docs/user/tutorial/TABLE_LOOKUP.md b/docs/user/tutorial/TABLE_LOOKUP.md index 600a46957..e96b60cc4 100644 --- a/docs/user/tutorial/TABLE_LOOKUP.md +++ b/docs/user/tutorial/TABLE_LOOKUP.md @@ -23,10 +23,10 @@ results in ```python -engine.run(0) == 2 -engine.run(1) == 1 -engine.run(2) == 3 -engine.run(3) == 0 +circuit.run(0) == 2 +circuit.run(1) == 1 +circuit.run(2) == 3 +circuit.run(3) == 0 ``` ## Fused table lookup @@ -49,14 +49,14 @@ results in ```python -engine.run(0) == 77 -engine.run(1) == 35 -engine.run(2) == 32 -engine.run(3) == 70 -engine.run(4) == 115 -engine.run(5) == 125 -engine.run(6) == 91 -engine.run(7) == 45 +circuit.run(0) == 77 +circuit.run(1) == 35 +circuit.run(2) == 32 +circuit.run(3) == 70 +circuit.run(4) == 115 +circuit.run(5) == 125 +circuit.run(6) == 91 +circuit.run(7) == 45 ``` Initially, the function is converted to this operation graph diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md index 7fbc98905..62d088baf 100644 --- a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -16,11 +16,11 @@ results in ```python -engine.run(3) == 27 -engine.run(0) == 0 -engine.run(1) == 90 -engine.run(10) == 91 -engine.run(60) == 58 +circuit.run(3) == 27 +circuit.run(0) == 0 +circuit.run(1) == 90 +circuit.run(10) == 91 +circuit.run(60) == 58 ``` ## Supported operations diff --git a/tests/common/test_fhe_circuit.py b/tests/common/test_fhe_circuit.py new file mode 100644 index 000000000..42e4d3bab --- /dev/null +++ b/tests/common/test_fhe_circuit.py @@ -0,0 +1,50 @@ +"""Test module for Circuit class""" + +import filecmp + +import concrete.numpy as hnp +from concrete.common.debugging import draw_graph, get_printable_graph + + +def test_circuit_str(): + """Test function for `__str__` method of `Circuit`""" + + def f(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + inputset = [(i,) for i in range(2 ** 3)] + circuit = hnp.compile_numpy_function(f, {"x": x}, inputset) + + assert str(circuit) == get_printable_graph(circuit.opgraph, show_data_types=True) + + +def test_circuit_draw(): + """Test function for `draw` method of `Circuit`""" + + def f(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + inputset = [(i,) for i in range(2 ** 3)] + circuit = hnp.compile_numpy_function(f, {"x": x}, inputset) + + assert filecmp.cmp(circuit.draw(), draw_graph(circuit.opgraph)) + assert filecmp.cmp(circuit.draw(vertical=False), draw_graph(circuit.opgraph, vertical=False)) + + +def test_circuit_run(): + """Test function for `run` method of `Circuit`""" + + def f(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + inputset = [(i,) for i in range(2 ** 3)] + circuit = hnp.compile_numpy_function(f, {"x": x}, inputset) + + for x in inputset: + assert circuit.run(*x) == circuit.engine.run(*x) From b93e916b1c7857029c868fc9dfe0efd0e06ca96b Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 4 Oct 2021 12:05:25 +0300 Subject: [PATCH 0359/1104] docs(user): create printing and drawing howto --- docs/index.rst | 1 + docs/user/howto/PRINTING_AND_DRAWING.md | 49 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docs/user/howto/PRINTING_AND_DRAWING.md diff --git a/docs/index.rst b/docs/index.rst index 7ddeea81a..e0e38bb9b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Concrete Framework's documentation :caption: How to user/howto/COMPILING_AND_EXECUTING.md + user/howto/PRINTING_AND_DRAWING.md user/howto/REDUCE_NEEDED_PRECISION.md user/howto/DEBUG_SUPPORT_SUBMIT_ISSUES.md user/howto/FAQ.md diff --git a/docs/user/howto/PRINTING_AND_DRAWING.md b/docs/user/howto/PRINTING_AND_DRAWING.md new file mode 100644 index 000000000..c660d9c47 --- /dev/null +++ b/docs/user/howto/PRINTING_AND_DRAWING.md @@ -0,0 +1,49 @@ +# Printing and Drawing + +Sometimes, it can be useful to print or draw fhe circuits, we provide methods to just do that. Please read [Compiling and Executing](../howto/COMPILING_AND_EXECUTING.md) before reading further to see how you can compile your function into an fhe circuit. + +## Printing + +To print your circuit, you can do the following: + + +```python +print(circuit) +``` + +## Drawing + +To draw your circuit, you can do the following: + + +```python +drawing = circuit.draw() +``` + +This method will draw the circuit on a temporary PNG file and return the path to this file. + +To show the drawing, you can use the following code in a jupyter notebook. + + +```python +from PIL import Image +drawing = Image.open(circuit.draw()) +drawing.show() +drawing.close() +``` + +Additionally, you can use the `show` option of the `draw` method to show the drawing with matplotlib. Beware that this will clear the matplotlib plots you have. + + +```python +circuit.draw(show=True) +``` + +Lastly, you can save the drawing to a specific path like this: + + +```python +destination = "/tmp/path/of/your/choice.png" +drawing = circuit.draw(save_to=destination) +assert drawing == destination +``` From 2515e9bf8a2d5a2dfb9563e40024f9049100fd39 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 4 Oct 2021 14:25:48 +0300 Subject: [PATCH 0360/1104] feat(script): replace '&' with 'and' within benchmark target names --- script/progress_tracker_utils/measure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 9d8016354..1afe36b25 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -18,6 +18,7 @@ def name_to_id(name): name = name.replace("*", "times") name = name.replace("/", "over") name = name.replace("%", "percent") + name = name.replace("&", "and") name = name.replace(" ", "-") name = name.replace("(", "") name = name.replace(")", "") From 7e3b0251fce6ec2aff650d12aa9b37fafb685804 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 4 Oct 2021 14:27:15 +0300 Subject: [PATCH 0361/1104] fix(script): use shutil.rmtree instead of os.unlink to remove measurement scripts folder --- script/progress_tracker_utils/measure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 1afe36b25..006713d0b 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -3,6 +3,7 @@ import argparse import json import os import pathlib +import shutil import subprocess import urllib @@ -322,7 +323,7 @@ def main(): # Delete the modified scripts if the user doesn't care if not args.keep: - os.unlink(".benchmarks/scripts") + shutil.rmtree(".benchmarks/scripts", ignore_errors=True) print() From 43743ffdbc818f25401badb94beb8e818fd4f411 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 4 Oct 2021 14:54:37 +0300 Subject: [PATCH 0362/1104] feat(benchmarks): add basic tensor operation benchmarks --- benchmarks/124_minus_x_tensor.py | 46 +++++++++++++++++++ benchmarks/x_minus_1_2_3.py | 46 +++++++++++++++++++ benchmarks/x_minus_1_2_3_broadcasted.py | 46 +++++++++++++++++++ benchmarks/x_minus_24_tensor.py | 46 +++++++++++++++++++ benchmarks/x_minus_y_broadcasted_tensors.py | 48 ++++++++++++++++++++ benchmarks/x_minus_y_tensor_and_scalar.py | 50 +++++++++++++++++++++ benchmarks/x_minus_y_tensors.py | 48 ++++++++++++++++++++ benchmarks/x_plus_1_2_3.py | 46 +++++++++++++++++++ benchmarks/x_plus_1_2_3_broadcasted.py | 46 +++++++++++++++++++ benchmarks/x_plus_42_tensor.py | 46 +++++++++++++++++++ benchmarks/x_plus_y_broadcasted_tensors.py | 48 ++++++++++++++++++++ benchmarks/x_plus_y_tensor_and_scalar.py | 50 +++++++++++++++++++++ benchmarks/x_plus_y_tensors.py | 48 ++++++++++++++++++++ benchmarks/x_times_1_2_3.py | 46 +++++++++++++++++++ benchmarks/x_times_1_2_3_broadcasted.py | 46 +++++++++++++++++++ benchmarks/x_times_7_tensor.py | 46 +++++++++++++++++++ benchmarks/x_times_y_broadcasted_tensors.py | 48 ++++++++++++++++++++ benchmarks/x_times_y_tensor_and_scalar.py | 50 +++++++++++++++++++++ benchmarks/x_times_y_tensors.py | 48 ++++++++++++++++++++ 19 files changed, 898 insertions(+) create mode 100644 benchmarks/124_minus_x_tensor.py create mode 100644 benchmarks/x_minus_1_2_3.py create mode 100644 benchmarks/x_minus_1_2_3_broadcasted.py create mode 100644 benchmarks/x_minus_24_tensor.py create mode 100644 benchmarks/x_minus_y_broadcasted_tensors.py create mode 100644 benchmarks/x_minus_y_tensor_and_scalar.py create mode 100644 benchmarks/x_minus_y_tensors.py create mode 100644 benchmarks/x_plus_1_2_3.py create mode 100644 benchmarks/x_plus_1_2_3_broadcasted.py create mode 100644 benchmarks/x_plus_42_tensor.py create mode 100644 benchmarks/x_plus_y_broadcasted_tensors.py create mode 100644 benchmarks/x_plus_y_tensor_and_scalar.py create mode 100644 benchmarks/x_plus_y_tensors.py create mode 100644 benchmarks/x_times_1_2_3.py create mode 100644 benchmarks/x_times_1_2_3_broadcasted.py create mode 100644 benchmarks/x_times_7_tensor.py create mode 100644 benchmarks/x_times_y_broadcasted_tensors.py create mode 100644 benchmarks/x_times_y_tensor_and_scalar.py create mode 100644 benchmarks/x_times_y_tensors.py diff --git a/benchmarks/124_minus_x_tensor.py b/benchmarks/124_minus_x_tensor.py new file mode 100644 index 000000000..5b5d035f9 --- /dev/null +++ b/benchmarks/124_minus_x_tensor.py @@ -0,0 +1,46 @@ +# Target: 124 - x (Tensor) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return 124 - x + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(6), shape=(3,)) + + inputset = [ + (np.array([36, 50, 24]),), + (np.array([41, 60, 51]),), + (np.array([25, 31, 24]),), + (np.array([34, 47, 27]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 6, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_minus_1_2_3.py b/benchmarks/x_minus_1_2_3.py new file mode 100644 index 000000000..7e692b735 --- /dev/null +++ b/benchmarks/x_minus_1_2_3.py @@ -0,0 +1,46 @@ +# Target: x - [1, 2, 3] + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x - np.array([1, 2, 3]) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]),), + (np.array([1, 3, 5]),), + (np.array([5, 7, 2]),), + (np.array([1, 7, 7]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(3, 2 ** 3, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_minus_1_2_3_broadcasted.py b/benchmarks/x_minus_1_2_3_broadcasted.py new file mode 100644 index 000000000..b67e1d138 --- /dev/null +++ b/benchmarks/x_minus_1_2_3_broadcasted.py @@ -0,0 +1,46 @@ +# Target: x - [1, 2, 3] (Broadcasted) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x - np.array([1, 2, 3]) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + + inputset = [ + (np.array([[4, 7, 7], [6, 2, 4]]),), + (np.array([[6, 2, 4], [1, 3, 1]]),), + (np.array([[6, 2, 4], [5, 7, 5]]),), + (np.array([[5, 7, 5], [4, 7, 7]]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(3, 2 ** 3, size=(2, 3)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_minus_24_tensor.py b/benchmarks/x_minus_24_tensor.py new file mode 100644 index 000000000..f7ec198cb --- /dev/null +++ b/benchmarks/x_minus_24_tensor.py @@ -0,0 +1,46 @@ +# Target: x - 24 (Tensor) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x - 24 + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(6), shape=(3,)) + + inputset = [ + (np.array([36, 50, 24]),), + (np.array([41, 60, 51]),), + (np.array([25, 31, 24]),), + (np.array([34, 47, 27]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(24, 2 ** 6, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_minus_y_broadcasted_tensors.py b/benchmarks/x_minus_y_broadcasted_tensors.py new file mode 100644 index 000000000..ea6408350 --- /dev/null +++ b/benchmarks/x_minus_y_broadcasted_tensors.py @@ -0,0 +1,48 @@ +# Target: x - y (Broadcasted Tensors) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x - y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + + inputset = [ + (np.array([6, 2, 4]), np.array([[5, 1, 3], [0, 0, 4]])), + (np.array([1, 3, 1]), np.array([[0, 3, 1], [1, 2, 1]])), + (np.array([5, 1, 2]), np.array([[5, 0, 2], [2, 1, 1]])), + (np.array([0, 7, 7]), np.array([[0, 5, 1], [0, 7, 2]])), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(4, 2 ** 3, size=(3,)) + sample_y = np.random.randint(0, 5, size=(2, 3)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_minus_y_tensor_and_scalar.py b/benchmarks/x_minus_y_tensor_and_scalar.py new file mode 100644 index 000000000..3ab299c65 --- /dev/null +++ b/benchmarks/x_minus_y_tensor_and_scalar.py @@ -0,0 +1,50 @@ +# Target: x - y (Tensor & Scalar) + +import random + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x - y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + inputset = [ + (np.array([6, 2, 4]), 2), + (np.array([1, 3, 1]), 1), + (np.array([5, 4, 7]), 4), + (np.array([5, 7, 6]), 5), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(3, 2 ** 3, size=(3,)) + sample_y = random.randint(0, 3) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_minus_y_tensors.py b/benchmarks/x_minus_y_tensors.py new file mode 100644 index 000000000..6c681e0bc --- /dev/null +++ b/benchmarks/x_minus_y_tensors.py @@ -0,0 +1,48 @@ +# Target: x - y (Tensors) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x - y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]), np.array([4, 1, 2])), + (np.array([1, 3, 1]), np.array([1, 1, 0])), + (np.array([5, 1, 2]), np.array([4, 1, 1])), + (np.array([0, 7, 7]), np.array([0, 7, 0])), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(3, 2 ** 3, size=(3,)) + sample_y = np.random.randint(0, 4, size=(3,)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_1_2_3.py b/benchmarks/x_plus_1_2_3.py new file mode 100644 index 000000000..de73fda65 --- /dev/null +++ b/benchmarks/x_plus_1_2_3.py @@ -0,0 +1,46 @@ +# Target: x + [1, 2, 3] + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x + np.array([1, 2, 3]) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]),), + (np.array([1, 3, 1]),), + (np.array([5, 1, 2]),), + (np.array([0, 7, 7]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_1_2_3_broadcasted.py b/benchmarks/x_plus_1_2_3_broadcasted.py new file mode 100644 index 000000000..9826714f4 --- /dev/null +++ b/benchmarks/x_plus_1_2_3_broadcasted.py @@ -0,0 +1,46 @@ +# Target: x + [1, 2, 3] (Broadcasted) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x + np.array([1, 2, 3]) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + + inputset = [ + (np.array([[0, 7, 7], [6, 2, 4]]),), + (np.array([[6, 2, 4], [1, 3, 1]]),), + (np.array([[6, 2, 4], [5, 1, 2]]),), + (np.array([[5, 1, 2], [0, 7, 7]]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(2, 3)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_tensor.py b/benchmarks/x_plus_42_tensor.py new file mode 100644 index 000000000..1829304e8 --- /dev/null +++ b/benchmarks/x_plus_42_tensor.py @@ -0,0 +1,46 @@ +# Target: x + 42 (Tensor) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]),), + (np.array([1, 3, 1]),), + (np.array([5, 1, 2]),), + (np.array([0, 7, 7]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_y_broadcasted_tensors.py b/benchmarks/x_plus_y_broadcasted_tensors.py new file mode 100644 index 000000000..c13aea795 --- /dev/null +++ b/benchmarks/x_plus_y_broadcasted_tensors.py @@ -0,0 +1,48 @@ +# Target: x + y (Broadcasted Tensors) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x + y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + + inputset = [ + (np.array([6, 2, 4]), np.array([[5, 1, 2], [0, 7, 7]])), + (np.array([1, 3, 1]), np.array([[0, 7, 7], [6, 2, 4]])), + (np.array([5, 1, 2]), np.array([[6, 2, 4], [1, 3, 1]])), + (np.array([0, 7, 7]), np.array([[1, 3, 1], [5, 1, 2]])), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + sample_y = np.random.randint(0, 2 ** 3, size=(2, 3)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_y_tensor_and_scalar.py b/benchmarks/x_plus_y_tensor_and_scalar.py new file mode 100644 index 000000000..e314a99e9 --- /dev/null +++ b/benchmarks/x_plus_y_tensor_and_scalar.py @@ -0,0 +1,50 @@ +# Target: x + y (Tensor & Scalar) + +import random + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x + y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + inputset = [ + (np.array([6, 2, 4]), 4), + (np.array([1, 3, 1]), 1), + (np.array([5, 1, 2]), 2), + (np.array([0, 7, 7]), 5), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + sample_y = random.randint(0, 2 ** 3 - 1) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_y_tensors.py b/benchmarks/x_plus_y_tensors.py new file mode 100644 index 000000000..16718cc5e --- /dev/null +++ b/benchmarks/x_plus_y_tensors.py @@ -0,0 +1,48 @@ +# Target: x + y (Tensors) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x + y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]), np.array([0, 7, 7])), + (np.array([1, 3, 1]), np.array([6, 2, 4])), + (np.array([5, 1, 2]), np.array([1, 3, 1])), + (np.array([0, 7, 7]), np.array([5, 1, 2])), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + sample_y = np.random.randint(0, 2 ** 3, size=(3,)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_times_1_2_3.py b/benchmarks/x_times_1_2_3.py new file mode 100644 index 000000000..90812272d --- /dev/null +++ b/benchmarks/x_times_1_2_3.py @@ -0,0 +1,46 @@ +# Target: x * [1, 2, 3] + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x * np.array([1, 2, 3]) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]),), + (np.array([1, 3, 1]),), + (np.array([5, 1, 2]),), + (np.array([0, 7, 7]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_times_1_2_3_broadcasted.py b/benchmarks/x_times_1_2_3_broadcasted.py new file mode 100644 index 000000000..f6b8cb665 --- /dev/null +++ b/benchmarks/x_times_1_2_3_broadcasted.py @@ -0,0 +1,46 @@ +# Target: x * [1, 2, 3] (Broadcasted) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x * np.array([1, 2, 3]) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + + inputset = [ + (np.array([[0, 7, 7], [6, 2, 4]]),), + (np.array([[6, 2, 4], [1, 3, 1]]),), + (np.array([[6, 2, 4], [5, 1, 2]]),), + (np.array([[5, 1, 2], [0, 7, 7]]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(2, 3)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_times_7_tensor.py b/benchmarks/x_times_7_tensor.py new file mode 100644 index 000000000..c1ad87b73 --- /dev/null +++ b/benchmarks/x_times_7_tensor.py @@ -0,0 +1,46 @@ +# Target: x * 7 (Tensor) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return x * 7 + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]),), + (np.array([1, 3, 1]),), + (np.array([5, 1, 2]),), + (np.array([0, 7, 7]),), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_times_y_broadcasted_tensors.py b/benchmarks/x_times_y_broadcasted_tensors.py new file mode 100644 index 000000000..89a6bf8df --- /dev/null +++ b/benchmarks/x_times_y_broadcasted_tensors.py @@ -0,0 +1,48 @@ +# Target: x * y (Broadcasted Tensors) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x * y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + + inputset = [ + (np.array([6, 2, 4]), np.array([[5, 1, 2], [0, 7, 7]])), + (np.array([1, 3, 1]), np.array([[0, 7, 7], [6, 2, 4]])), + (np.array([5, 1, 2]), np.array([[6, 2, 4], [1, 3, 1]])), + (np.array([0, 7, 7]), np.array([[1, 3, 1], [5, 1, 2]])), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + sample_y = np.random.randint(0, 2 ** 3, size=(2, 3)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_times_y_tensor_and_scalar.py b/benchmarks/x_times_y_tensor_and_scalar.py new file mode 100644 index 000000000..4a13b9947 --- /dev/null +++ b/benchmarks/x_times_y_tensor_and_scalar.py @@ -0,0 +1,50 @@ +# Target: x * y (Tensor & Scalar) + +import random + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x * y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + + inputset = [ + (np.array([6, 2, 4]), 4), + (np.array([1, 3, 1]), 1), + (np.array([5, 1, 2]), 2), + (np.array([0, 7, 7]), 5), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + sample_y = random.randint(0, 5) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_times_y_tensors.py b/benchmarks/x_times_y_tensors.py new file mode 100644 index 000000000..118a5d3d9 --- /dev/null +++ b/benchmarks/x_times_y_tensors.py @@ -0,0 +1,48 @@ +# Target: x * y (Tensors) + +import numpy as np + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return x * y + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + + inputset = [ + (np.array([6, 2, 4]), np.array([0, 7, 7])), + (np.array([1, 3, 1]), np.array([6, 2, 4])), + (np.array([5, 1, 2]), np.array([1, 3, 1])), + (np.array([0, 7, 7]), np.array([5, 1, 2])), + ] + + # Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + # Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(3,)) + sample_y = np.random.randint(0, 2 ** 3, size=(3,)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # Measure: End + + if result_i == label_i: + correct += 1 + + # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + + +if __name__ == "__main__": + main() From 1fc9a36ab661767a05b81e73c3c69ee968d0fa45 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 5 Oct 2021 12:49:33 +0200 Subject: [PATCH 0363/1104] chore: upgrade nbmake to version without pathlib - it breaks poetry installs with a deprecated dependency - opened PR to solve: https://github.com/treebeardtech/nbmake/pull/48 - upgrade dependencies at the same time - re-order installation steps for make setup_env to first install pip --- Makefile | 2 +- poetry.lock | 527 +++++++++++++++++++++++++------------------------ pyproject.toml | 32 ++- 3 files changed, 290 insertions(+), 271 deletions(-) diff --git a/Makefile b/Makefile index 478c5ad39..2b60d85bd 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,9 @@ SRC_DIR:=concrete NOTEBOOKS_DIR:=docs/user/advanced_examples setup_env: - poetry install poetry run python -m pip install -U pip wheel poetry run python -m pip install -U --force-reinstall setuptools + poetry install poetry run python -m pip install -r torch_requirements.txt \ -f https://download.pytorch.org/whl/torch_stable.html .PHONY: setup_env diff --git a/poetry.lock b/poetry.lock index 889abbd1b..322f796c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,14 +6,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "appnope" version = "0.1.2" @@ -40,7 +32,7 @@ tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] [[package]] name = "astroid" -version = "2.7.3" +version = "2.8.0" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -48,6 +40,7 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<1.13" [[package]] @@ -93,23 +86,25 @@ python-versions = "*" [[package]] name = "black" -version = "21.7b0" +version = "21.9b0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -appdirs = "*" click = ">=7.1.2" mypy-extensions = ">=0.4.3" -pathspec = ">=0.8.1,<1" +pathspec = ">=0.9.0,<1" +platformdirs = ">=2" regex = ">=2020.1.8" tomli = ">=0.2.6,<2.0.0" +typing-extensions = ">=3.10.0.0" [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] @@ -155,7 +150,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.5" +version = "2.0.6" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -185,14 +180,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "5.5" +version = "6.0" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=3.6" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] -toml = ["toml"] +toml = ["tomli"] [[package]] name = "cycler" @@ -205,6 +203,14 @@ python-versions = "*" [package.dependencies] six = "*" +[[package]] +name = "debugpy" +version = "1.5.0" +description = "An implementation of the Debug Adapter Protocol for Python" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + [[package]] name = "decorator" version = "5.1.0" @@ -223,22 +229,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.2.1" -description = "Automatically find diff lines that need test coverage." +version = "6.4.1" +description = "Run coverage and linting reports on diffs" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.6.2,<4.0.0" [package.dependencies] chardet = ">=3.0.0" Jinja2 = ">=2.7.1" -jinja2-pluralize = "*" -pluggy = "*" -pygments = "*" +jinja2_pluralize = ">=0.3.0,<0.4.0" +pluggy = ">=0.13.1,<2" +Pygments = ">=2.9.0,<3.0.0" + +[package.extras] +toml = ["tomli (>=1.2.1,<2.0.0)"] [[package]] name = "docutils" -version = "0.16" +version = "0.17.1" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false @@ -267,7 +276,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-bugbear" -version = "21.9.1" +version = "21.9.2" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -318,25 +327,28 @@ python-versions = "*" [[package]] name = "ipykernel" -version = "5.5.5" +version = "6.4.1" description = "IPython Kernel for Jupyter" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [package.dependencies] appnope = {version = "*", markers = "platform_system == \"Darwin\""} -ipython = ">=5.0.0" -jupyter-client = "*" -tornado = ">=4.2" -traitlets = ">=4.1.0" +debugpy = ">=1.0.0,<2.0" +ipython = ">=7.23.1,<8.0" +ipython-genutils = "*" +jupyter-client = "<8.0" +matplotlib-inline = ">=0.1.0,<0.2.0" +tornado = ">=4.2,<7.0" +traitlets = ">=4.1.0,<6.0" [package.extras] -test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "jedi (<=0.17.2)"] +test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "nose", "ipyparallel"] [[package]] name = "ipython" -version = "7.27.0" +version = "7.28.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -425,7 +437,7 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [[package]] name = "jinja2" -version = "3.0.1" +version = "3.0.2" description = "A very fast and expressive template engine." category = "dev" optional = false @@ -451,20 +463,19 @@ jinja2 = ">=2.4" [[package]] name = "jsonschema" -version = "3.2.0" +version = "4.0.1" description = "An implementation of JSON Schema validation for Python" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] attrs = ">=17.4.0" -pyrsistent = ">=0.14.0" -six = ">=1.11.0" +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" [package.extras] -format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] [[package]] name = "jupyter" @@ -484,7 +495,7 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "7.0.2" +version = "7.0.5" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false @@ -523,14 +534,14 @@ test = ["pexpect"] [[package]] name = "jupyter-core" -version = "4.7.1" +version = "4.8.1" description = "Jupyter core package. A base package on which Jupyter projects rely." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\""} +pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} traitlets = "*" [[package]] @@ -737,7 +748,7 @@ test = ["codecov", "coverage", "ipython", "ipykernel", "ipywidgets", "pytest (>= [[package]] name = "nbconvert" -version = "6.1.0" +version = "6.2.0" description = "Converting Jupyter Notebooks" category = "dev" optional = false @@ -759,11 +770,11 @@ testpath = "*" traitlets = ">=5.0" [package.extras] -all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] +all = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)", "tornado (>=4.0)", "sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] docs = ["sphinx (>=1.5.1)", "sphinx-rtd-theme", "nbsphinx (>=0.2.12)", "ipython"] serve = ["tornado (>=4.0)"] -test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.2)"] -webpdf = ["pyppeteer (==0.2.2)"] +test = ["pytest", "pytest-cov", "pytest-dependency", "ipykernel", "ipywidgets (>=7)", "pyppeteer (==0.2.6)"] +webpdf = ["pyppeteer (==0.2.6)"] [[package]] name = "nbformat" @@ -785,17 +796,16 @@ test = ["check-manifest", "fastjsonschema", "testpath", "pytest", "pytest-cov"] [[package]] name = "nbmake" -version = "0.5" +version = "0.9" description = "Pytest plugin for testing notebooks" category = "dev" optional = false python-versions = ">=3.6.1,<4.0.0" [package.dependencies] -ipykernel = ">=5.4.0,<6.0.0" +ipykernel = ">=5.4.0" nbclient = ">=0.3,<1.0" nbformat = ">=5.0.8,<6.0.0" -pathlib = ">=1.0.1,<2.0.0" pydantic = ">=1.7.2,<2.0.0" Pygments = ">=2.7.3,<3.0.0" pytest = ">=6.1.2,<7.0.0" @@ -841,7 +851,7 @@ test = ["pytest (>=6.2)", "pytest-cov (>=2.12)", "codecov (>=2.1)"] [[package]] name = "notebook" -version = "6.4.3" +version = "6.4.4" description = "A web-based notebook environment for interactive computing" category = "dev" optional = false @@ -907,14 +917,6 @@ python-versions = ">=3.6" qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] -[[package]] -name = "pathlib" -version = "1.0.1" -description = "Object-oriented filesystem paths" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "pathspec" version = "0.9.0" @@ -952,7 +954,7 @@ python-versions = ">=3.6" [[package]] name = "platformdirs" -version = "2.3.0" +version = "2.4.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -1102,19 +1104,20 @@ python-versions = ">=3.7" [[package]] name = "pylint" -version = "2.10.2" +version = "2.11.1" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.7.2,<2.8" +astroid = ">=2.8.0,<2.9" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" platformdirs = ">=2.2.0" toml = ">=0.7.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [[package]] name = "pyparsing" @@ -1155,16 +1158,15 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.12.1" +version = "3.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -coverage = ">=5.2.1" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" -toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] @@ -1193,7 +1195,7 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2021.1" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -1225,7 +1227,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "pyzmq" -version = "22.2.1" +version = "22.3.0" description = "Python bindings for 0MQ" category = "dev" optional = false @@ -1259,7 +1261,7 @@ test = ["flaky", "pytest", "pytest-qt"] [[package]] name = "qtpy" -version = "1.11.1" +version = "1.11.2" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." category = "dev" optional = false @@ -1267,7 +1269,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" [[package]] name = "regex" -version = "2021.8.28" +version = "2021.9.30" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -1505,7 +1507,7 @@ python-versions = ">= 3.5" [[package]] name = "tqdm" -version = "4.62.2" +version = "4.62.3" description = "Fast, Extensible Progress Meter" category = "dev" optional = false @@ -1540,7 +1542,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.6" +version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -1600,17 +1602,13 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "5d1a54ac0a00d494678a66161c3914bdadd046bd4a5b5e886cf336f966b09da7" +content-hash = "f0205455ef0cf713936e92ef934fc6f850ca9aa46cf59c4151cc9deff4f37958" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] appnope = [ {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, @@ -1629,8 +1627,8 @@ argon2-cffi = [ {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:566ffb581bbd9db5562327aee71b2eda24a1c15b23a356740abe3c011bbe0dcb"}, ] astroid = [ - {file = "astroid-2.7.3-py3-none-any.whl", hash = "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"}, - {file = "astroid-2.7.3.tar.gz", hash = "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c"}, + {file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"}, + {file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1649,8 +1647,8 @@ backcall = [ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] black = [ - {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, - {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, + {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, + {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, ] bleach = [ {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, @@ -1712,8 +1710,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.5.tar.gz", hash = "sha256:7098e7e862f6370a2a8d1a6398cd359815c45d12626267652c3f13dec58e2367"}, - {file = "charset_normalizer-2.0.5-py3-none-any.whl", hash = "sha256:fa471a601dfea0f492e4f4fca035cd82155e65dc45c9b83bf4322dfab63755dd"}, + {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, + {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, ] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, @@ -1724,63 +1722,69 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, - {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, - {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, - {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, - {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, - {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, - {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, - {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, - {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, - {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, - {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, - {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, - {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, - {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, - {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, - {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, - {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, - {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, - {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, - {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, - {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, - {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, - {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, - {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, - {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, - {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, - {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, - {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, - {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, - {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, - {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, - {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, - {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, - {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, - {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, - {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, - {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, + {file = "coverage-6.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3dfb23cc180b674a11a559183dff9655beb9da03088f3fe3c4f3a6d200c86f05"}, + {file = "coverage-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5dd5ae0a9cd55d71f1335c331e9625382239b8cede818fb62d8d2702336dbf8"}, + {file = "coverage-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8426fec5ad5a6e8217921716b504e9b6e1166dc147e8443b4855e329db686282"}, + {file = "coverage-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aa5d4d43fa18cc9d0c6e02a83de0b9729b5451a9066574bd276481474f0a53ab"}, + {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78dd3eeb8f5ff26d2113c41836bac04a9ea91be54c346826b54a373133c8c53"}, + {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:581fddd2f883379bd5af51da9233e0396b6519f3d3eeae4fb88867473be6d56e"}, + {file = "coverage-6.0-cp310-cp310-win32.whl", hash = "sha256:43bada49697a62ffa0283c7f01bbc76aac562c37d4bb6c45d56dd008d841194e"}, + {file = "coverage-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa816e97cfe1f691423078dffa39a18106c176f28008db017b3ce3e947c34aa5"}, + {file = "coverage-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5c191e01b23e760338f19d8ba2470c0dad44c8b45e41ac043b2db84efc62f695"}, + {file = "coverage-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274a612f67f931307706b60700f1e4cf80e1d79dff6c282fc9301e4565e78724"}, + {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9dbfcbc56d8de5580483cf2caff6a59c64d3e88836cbe5fb5c20c05c29a8808"}, + {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e63490e8a6675cee7a71393ee074586f7eeaf0e9341afd006c5d6f7eec7c16d7"}, + {file = "coverage-6.0-cp36-cp36m-win32.whl", hash = "sha256:72f8c99f1527c5a8ee77c890ea810e26b39fd0b4c2dffc062e20a05b2cca60ef"}, + {file = "coverage-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:88f1810eb942e7063d051d87aaaa113eb5fd5a7fd2cda03a972de57695b8bb1a"}, + {file = "coverage-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:befb5ffa9faabef6dadc42622c73de168001425258f0b7e402a2934574e7a04b"}, + {file = "coverage-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dbda34e8e26bd86606ba8a9c13ccb114802e01758a3d0a75652ffc59a573220"}, + {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4ee5815c776dfa3958ba71c7cd4cdd8eb40d79358a18352feb19562fe4408c4"}, + {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d82cbef1220703ce56822be7fbddb40736fc1a928ac893472df8aff7421ae0aa"}, + {file = "coverage-6.0-cp37-cp37m-win32.whl", hash = "sha256:d795a2c92fe8cb31f6e9cd627ee4f39b64eb66bf47d89d8fcf7cb3d17031c887"}, + {file = "coverage-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6e216e4021c934246c308fd3e0d739d9fa8a3f4ea414f584ab90ef9c1592f282"}, + {file = "coverage-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8305e14112efb74d0b5fec4df6e41cafde615c2392a7e51c84013cafe945842c"}, + {file = "coverage-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4865dc4a7a566147cbdc2b2f033a6cccc99a7dcc89995137765c384f6c73110b"}, + {file = "coverage-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:25df2bc53a954ba2ccf230fa274d1de341f6aa633d857d75e5731365f7181749"}, + {file = "coverage-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08fd55d2e00dac4c18a2fa26281076035ec86e764acdc198b9185ce749ada58f"}, + {file = "coverage-6.0-cp38-cp38-win32.whl", hash = "sha256:11ce082eb0f7c2bbfe96f6c8bcc3a339daac57de4dc0f3186069ec5c58da911c"}, + {file = "coverage-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7844a8c6a0fee401edbf578713c2473e020759267c40261b294036f9d3eb6a2d"}, + {file = "coverage-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bea681309bdd88dd1283a8ba834632c43da376d9bce05820826090aad80c0126"}, + {file = "coverage-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e735ab8547d8a1fe8e58dd765d6f27ac539b395f52160d767b7189f379f9be7a"}, + {file = "coverage-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7593a49300489d064ebb6c58539f52cbbc4a2e6a4385de5e92cae1563f88a425"}, + {file = "coverage-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adb0f4c3c8ba8104378518a1954cbf3d891a22c13fd0e0bf135391835f44f288"}, + {file = "coverage-6.0-cp39-cp39-win32.whl", hash = "sha256:8da0c4a26a831b392deaba5fdd0cd7838d173b47ce2ec3d0f37be630cb09ef6e"}, + {file = "coverage-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7af2f8e7bb54ace984de790e897f858e88068d8fbc46c9490b7c19c59cf51822"}, + {file = "coverage-6.0-pp36-none-any.whl", hash = "sha256:82b58d37c47d93a171be9b5744bcc96a0012cbf53d5622b29a49e6be2097edd7"}, + {file = "coverage-6.0-pp37-none-any.whl", hash = "sha256:fff04bfefb879edcf616f1ce5ea6f4a693b5976bdc5e163f8464f349c25b59f0"}, + {file = "coverage-6.0.tar.gz", hash = "sha256:17983f6ccc47f4864fd16d20ff677782b23d1207bf222d10e4d676e4636b0872"}, ] cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] +debugpy = [ + {file = "debugpy-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:098753d30232d1e4264eee37e1ddd5d106dc5c4bc6d8d7f4dadad9e44736cd48"}, + {file = "debugpy-1.5.0-cp310-cp310-win32.whl", hash = "sha256:33e8a9b4949be8b4f5fcfff07e24bd63c565060659f1c79773c08d19eee012f2"}, + {file = "debugpy-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef71eb8eb276370f8e74ab3f8c7648bbdc9aabac814a5b2840c8dd38a7bc7251"}, + {file = "debugpy-1.5.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:dd0e8d5e099444c22b27511dafd48e8bdcd7051b811ddd0ab2062965fe36ac80"}, + {file = "debugpy-1.5.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:990228f15de4ccbc52c2accf41a63b3b8d0a01e3de9876e02e77e487c4b1ffab"}, + {file = "debugpy-1.5.0-cp36-cp36m-win32.whl", hash = "sha256:77b5233b23a248cd930bf03ecd684da065c6e7d2a57d137516b6fa1698a58317"}, + {file = "debugpy-1.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c3184666cfe1768bf110f8075bafea59d2afce3cc54f4c501f2371c7238bc69d"}, + {file = "debugpy-1.5.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1283e418f595262d11abc5fae6a3ac629c5fc3b44d3988511ea755414aab3062"}, + {file = "debugpy-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a03051ba4fdf6720ee83a42e9f803e3a0b69a48b00436b97d16aeda49d28a8bf"}, + {file = "debugpy-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:cdaf6baaf8176644e752aed321b3f810dcf8b0439709f7edd9ae542f849a639b"}, + {file = "debugpy-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be7ca2baef5a634dfbd086d9c1d6b5e0783c6d0f6d0a004b43d36f625d4fc0a9"}, + {file = "debugpy-1.5.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:72093ea83226d5264b3697b948c07a3cfcc4953da14a78a50c4e623a2bb99ad8"}, + {file = "debugpy-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ce0794d50391c87813bb148548c34dc638fb4d58198d275334968f63c088aa69"}, + {file = "debugpy-1.5.0-cp38-cp38-win32.whl", hash = "sha256:de56775b3dbbfc02bc9fb0682da4a960e0a5bada699eac5e22e0723c4107ec9f"}, + {file = "debugpy-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:82c4fa1293981a28c435d196a3714e06df761daff0da3336234475ceff1b042c"}, + {file = "debugpy-1.5.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:8e7391a08a351adce6e5154ed35e4cf90c5f3c10dbf7c8f6a234faef300588d6"}, + {file = "debugpy-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dacdb0a3377063d638bd8736c80b7274ae341ce778fec0f883ef1cbb79538bf2"}, + {file = "debugpy-1.5.0-cp39-cp39-win32.whl", hash = "sha256:fda623aa1036b34d554a1225a09cae6bf02b06c0ad903a9f0b8ac3cb74eddc15"}, + {file = "debugpy-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:9f3bed64027bd80a8fe1f35491ec0ec2d2c85f1e63dac7c0311e400bfe58cf05"}, + {file = "debugpy-1.5.0-py2.py3-none-any.whl", hash = "sha256:f058c204341fd7ff800ee0edafc106ca0fb1c9857e8a8895a6e04cca3ddcb7bf"}, + {file = "debugpy-1.5.0.zip", hash = "sha256:86febd61fc351cee926060eef008e242b7259957d71d25eef82860d0cc59b4dc"}, +] decorator = [ {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, @@ -1790,12 +1794,12 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.2.1-py3-none-any.whl", hash = "sha256:cfe6333c9b83a28f91214e06e3a86108aeeb6a9a31233944ce8ef236121ba899"}, - {file = "diff_cover-6.2.1.tar.gz", hash = "sha256:b22fe97d118fadb2528bbc0a1f9fc370491b29b1b3fe2e2f1cdb94bf028814c7"}, + {file = "diff_cover-6.4.1-py3-none-any.whl", hash = "sha256:0aa34983401d42f6f69a9c9be0fa5fe6f0cb9fa8ada4cca3a0145d6d8dd9bf74"}, + {file = "diff_cover-6.4.1.tar.gz", hash = "sha256:9d0296e25ca21b2235e70a0001acda498f52896b3453ea44d04cf53ceeb5ef72"}, ] docutils = [ - {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, - {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, @@ -1806,8 +1810,8 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-21.9.1.tar.gz", hash = "sha256:2f60c8ce0dc53d51da119faab2d67dea978227f0f92ed3c44eb7d65fb2e06a96"}, - {file = "flake8_bugbear-21.9.1-py36.py37.py38-none-any.whl", hash = "sha256:45bfdccfb9f2d8aa140e33cac8f46f1e38215c13d5aa8650e7e188d84e2f94c6"}, + {file = "flake8-bugbear-21.9.2.tar.gz", hash = "sha256:db9a09893a6c649a197f5350755100bb1dd84f110e60cf532fdfa07e41808ab2"}, + {file = "flake8_bugbear-21.9.2-py36.py37.py38-none-any.whl", hash = "sha256:4f7eaa6f05b7d7ea4cbbde93f7bcdc5438e79320fa1ec420d860c181af38b769"}, ] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, @@ -1826,12 +1830,12 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] ipykernel = [ - {file = "ipykernel-5.5.5-py3-none-any.whl", hash = "sha256:29eee66548ee7c2edb7941de60c0ccf0a7a8dd957341db0a49c5e8e6a0fcb712"}, - {file = "ipykernel-5.5.5.tar.gz", hash = "sha256:e976751336b51082a89fc2099fb7f96ef20f535837c398df6eab1283c2070884"}, + {file = "ipykernel-6.4.1-py3-none-any.whl", hash = "sha256:a3f6c2dda2ecf63b37446808a70ed825fea04790779ca524889c596deae0def8"}, + {file = "ipykernel-6.4.1.tar.gz", hash = "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"}, ] ipython = [ - {file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"}, - {file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"}, + {file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"}, + {file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -1850,16 +1854,16 @@ jedi = [ {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, + {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, ] jinja2-pluralize = [ {file = "jinja2_pluralize-0.3.0-py2.py3-none-any.whl", hash = "sha256:4fec874a591014774d4c66cb7f65314390731bfc57db4c27119db61aa93b2bc4"}, {file = "jinja2_pluralize-0.3.0.tar.gz", hash = "sha256:df5c2d5017b9b54c0a66cb790cca9fc08945837c3dbfc323589203f1ffb73c1c"}, ] jsonschema = [ - {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, - {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, + {file = "jsonschema-4.0.1-py3-none-any.whl", hash = "sha256:9938802041347f2c62cad2aef59e9a0826cd34584f3609db950efacb4dbf6518"}, + {file = "jsonschema-4.0.1.tar.gz", hash = "sha256:48f4e74f8bec0c2f75e9fcfffa264e78342873e1b57e2cfeae54864cc5e9e4dd"}, ] jupyter = [ {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, @@ -1867,16 +1871,16 @@ jupyter = [ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, ] jupyter-client = [ - {file = "jupyter_client-7.0.2-py3-none-any.whl", hash = "sha256:37a30c13d3655b819add61c830594090af7fca40cd2d74f41cad9e2e12118501"}, - {file = "jupyter_client-7.0.2.tar.gz", hash = "sha256:0c6cabd07e003a2e9692394bf1ae794188ad17d2e250ed747232d7a473aa772c"}, + {file = "jupyter_client-7.0.5-py3-none-any.whl", hash = "sha256:124a6e6979c38999d9153b1c4d1808c4c820a45066d5ed1857a5b59c04ffccb3"}, + {file = "jupyter_client-7.0.5.tar.gz", hash = "sha256:382aca66dcaf96d7eaaa6c546d57cdf8b3b1cf5bc1f2704c58a1d8d244f1163d"}, ] jupyter-console = [ {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, {file = "jupyter_console-6.4.0.tar.gz", hash = "sha256:242248e1685039cd8bff2c2ecb7ce6c1546eb50ee3b08519729e6e881aec19c7"}, ] jupyter-core = [ - {file = "jupyter_core-4.7.1-py3-none-any.whl", hash = "sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e"}, - {file = "jupyter_core-4.7.1.tar.gz", hash = "sha256:79025cb3225efcd36847d0840f3fc672c0abd7afd0de83ba8a1d3837619122b4"}, + {file = "jupyter_core-4.8.1-py3-none-any.whl", hash = "sha256:8dd262ec8afae95bd512518eb003bc546b76adbf34bf99410e9accdf4be9aa3a"}, + {file = "jupyter_core-4.8.1.tar.gz", hash = "sha256:ef210dcb4fca04de07f2ead4adf408776aca94d17151d6f750ad6ded0b91ea16"}, ] jupyterlab-pygments = [ {file = "jupyterlab_pygments-0.1.2-py2.py3-none-any.whl", hash = "sha256:abfb880fd1561987efaefcb2d2ac75145d2a5d0139b1876d5be806e32f630008"}, @@ -1965,12 +1969,22 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1979,14 +1993,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1996,6 +2017,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2077,16 +2101,16 @@ nbclient = [ {file = "nbclient-0.5.4.tar.gz", hash = "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce"}, ] nbconvert = [ - {file = "nbconvert-6.1.0-py3-none-any.whl", hash = "sha256:37cd92ff2ae6a268e62075ff8b16129e0be4939c4dfcee53dc77cc8a7e06c684"}, - {file = "nbconvert-6.1.0.tar.gz", hash = "sha256:d22a8ff202644d31db254d24d52c3a96c82156623fcd7c7f987bba2612303ec9"}, + {file = "nbconvert-6.2.0-py3-none-any.whl", hash = "sha256:b1b9dc4f1ff6cafae0e6d91f42fb9046fdc32e6beb6d7e2fa2cd7191ad535240"}, + {file = "nbconvert-6.2.0.tar.gz", hash = "sha256:16ceecd0afaa8fd26c245fa32e2c52066c02f13aa73387fffafd84750baea863"}, ] nbformat = [ {file = "nbformat-5.1.3-py3-none-any.whl", hash = "sha256:eb8447edd7127d043361bc17f2f5a807626bc8e878c7709a1c647abda28a9171"}, {file = "nbformat-5.1.3.tar.gz", hash = "sha256:b516788ad70771c6250977c1374fcca6edebe6126fd2adb5a69aa5c2356fd1c8"}, ] nbmake = [ - {file = "nbmake-0.5-py3-none-any.whl", hash = "sha256:8a0b3ce9ca26320165c6de532c3d36445da1dd53c2c8fac4870ed900b3cbe538"}, - {file = "nbmake-0.5.tar.gz", hash = "sha256:da9bf1bbc377c9d1d697f99952834017c39b4983e7e482a038dec705955a8ae9"}, + {file = "nbmake-0.9-py3-none-any.whl", hash = "sha256:fcae85ec12b077cbb5a23f67091748a5a2e68ce35fe1b5fa14789d2067c7eb7f"}, + {file = "nbmake-0.9.tar.gz", hash = "sha256:f2d8542be4763310c264be7caa12b36932ea40b3019dc66632ebda2a71589768"}, ] nbsphinx = [ {file = "nbsphinx-0.8.7-py3-none-any.whl", hash = "sha256:8862f291f98c1a163bdb5bac8adf25c61585a81575ac5c613320c6f3fe5c472f"}, @@ -2101,8 +2125,8 @@ networkx = [ {file = "networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51"}, ] notebook = [ - {file = "notebook-6.4.3-py3-none-any.whl", hash = "sha256:b50eafa8208d5db966efd1caa4076b4dfc51815e02a805b32ecd717e9e6cc071"}, - {file = "notebook-6.4.3.tar.gz", hash = "sha256:e6b6dfed36b00cf950f63c0d42e947c101d4258aec21624de62b9e0c11ed5c0d"}, + {file = "notebook-6.4.4-py3-none-any.whl", hash = "sha256:33488bdcc5cbef23c3cfa12cd51b0b5459a211945b5053d17405980611818149"}, + {file = "notebook-6.4.4.tar.gz", hash = "sha256:26b0095c568e307a310fd78818ad8ebade4f00462dada4c0e34cbad632b9085d"}, ] numpy = [ {file = "numpy-1.21.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52a664323273c08f3b473548bf87c8145b7513afd63e4ebba8496ecd3853df13"}, @@ -2148,9 +2172,6 @@ parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, ] -pathlib = [ - {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, -] pathspec = [ {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, @@ -2219,8 +2240,8 @@ pillow = [ {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] platformdirs = [ - {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, - {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -2323,8 +2344,8 @@ pygraphviz = [ {file = "pygraphviz-1.7.zip", hash = "sha256:a7bec6609f37cf1e64898c59f075afd659106cf9356c5f387cecaa2e0cdb2304"}, ] pylint = [ - {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, - {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, + {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, + {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -2358,8 +2379,8 @@ pytest = [ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -2370,8 +2391,8 @@ python-dotenv = [ {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, ] pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pywin32 = [ {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, @@ -2424,94 +2445,94 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] pyzmq = [ - {file = "pyzmq-22.2.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:d60a407663b7c2af781ab7f49d94a3d379dd148bb69ea8d9dd5bc69adf18097c"}, - {file = "pyzmq-22.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:631f932fb1fa4b76f31adf976f8056519bc6208a3c24c184581c3dd5be15066e"}, - {file = "pyzmq-22.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0471d634c7fe48ff7d3849798da6c16afc71676dd890b5ae08eb1efe735c6fec"}, - {file = "pyzmq-22.2.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f520e9fee5d7a2e09b051d924f85b977c6b4e224e56c0551c3c241bbeeb0ad8d"}, - {file = "pyzmq-22.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1b6619ceb33a8907f1cb82ff8afc8a133e7a5f16df29528e919734718600426"}, - {file = "pyzmq-22.2.1-cp310-cp310-win32.whl", hash = "sha256:31c5dfb6df5148789835128768c01bf6402eb753d06f524f12f6786caf96fb44"}, - {file = "pyzmq-22.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:4842a8263cbaba6fce401bbe4e2b125321c401a01714e42624dabc554bfc2629"}, - {file = "pyzmq-22.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b921758f8b5098faa85f341bbdd5e36d5339de5e9032ca2b07d8c8e7bec5069b"}, - {file = "pyzmq-22.2.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:240b83b3a8175b2f616f80092cbb019fcd5c18598f78ffc6aa0ae9034b300f14"}, - {file = "pyzmq-22.2.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:da7f7f3bb08bcf59a6b60b4e53dd8f08bb00c9e61045319d825a906dbb3c8fb7"}, - {file = "pyzmq-22.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e66025b64c4724ba683d6d4a4e5ee23de12fe9ae683908f0c7f0f91b4a2fd94e"}, - {file = "pyzmq-22.2.1-cp36-cp36m-win32.whl", hash = "sha256:50d007d5702171bc810c1e74498fa2c7bc5b50f9750697f7fd2a3e71a25aad91"}, - {file = "pyzmq-22.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b4a51c7d906dc263a0cc5590761e53e0a68f2c2fefe549cbef21c9ee5d2d98a4"}, - {file = "pyzmq-22.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:93705cb90baa9d6f75e8448861a1efd3329006f79095ab18846bd1eaa342f7c3"}, - {file = "pyzmq-22.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:620b0abb813958cb3ecb5144c177e26cde92fee6f43c4b9de6b329515532bf27"}, - {file = "pyzmq-22.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2dd3896b3c952cf6c8013deda53c1df16bf962f355b5503d23521e0f6403ae3d"}, - {file = "pyzmq-22.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e9c030222893afa86881d7485d3e841969760a16004bd23e9a83cca28b42778"}, - {file = "pyzmq-22.2.1-cp37-cp37m-win32.whl", hash = "sha256:262f470e7acde18b7217aac78d19d2e29ced91a5afbeb7d98521ebf26461aa7e"}, - {file = "pyzmq-22.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:246f27b88722cfa729bb04881e94484e40b085720d728c1b05133b3f331b0b7b"}, - {file = "pyzmq-22.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0d17bac19e934e9f547a8811b7c2a32651a7840f38086b924e2e3dcb2fae5c3a"}, - {file = "pyzmq-22.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5933d1f4087de6e52906f72d92e1e4dcc630d371860b92c55d7f7a4b815a664c"}, - {file = "pyzmq-22.2.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac4497e4b7d134ee53ce5532d9cc3b640d6e71806a55062984e0c99a2f88f465"}, - {file = "pyzmq-22.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66375a6094af72a6098ed4403b15b4db6bf00013c6febc1baa832e7abda827f4"}, - {file = "pyzmq-22.2.1-cp38-cp38-win32.whl", hash = "sha256:b2c16d20bd0aef8e57bc9505fdd80ea0d6008020c3740accd96acf1b3d1b5347"}, - {file = "pyzmq-22.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff345d48940c834168f81fa1d4724675099f148f1ab6369748c4d712ed71bf7c"}, - {file = "pyzmq-22.2.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:f5c84c5de9a773bbf8b22c51e28380999ea72e5e85b4db8edf5e69a7a0d4d9f9"}, - {file = "pyzmq-22.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2534a036b777f957bd6b89b55fb2136775ca2659fb0f1c85036ba78d17d86fd5"}, - {file = "pyzmq-22.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a649065413ba4eab92a783a7caa4de8ce14cf46ba8a2a09951426143f1298adb"}, - {file = "pyzmq-22.2.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9cb0bd3a3cb7ccad3caa1d7b0d18ba71ed3a4a3610028e506a4084371d4d223"}, - {file = "pyzmq-22.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4428302c389fffc0c9c07a78cad5376636b9d096f332acfe66b321ae9ff2c63"}, - {file = "pyzmq-22.2.1-cp39-cp39-win32.whl", hash = "sha256:6a5b4566f66d953601d0d47d4071897f550a265bafd52ebcad5ac7aad3838cbb"}, - {file = "pyzmq-22.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:89200ab6ef9081c72a04ed84c52a50b60dcb0655375aeedb40689bc7c934715e"}, - {file = "pyzmq-22.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed67df4eaa99a20d162d76655bda23160abdf8abf82a17f41dfd3962e608dbcc"}, - {file = "pyzmq-22.2.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:021e22a8c58ab294bd4b96448a2ca4e716e1d76600192ff84c33d71edb1fbd37"}, - {file = "pyzmq-22.2.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:200ac096cee5499964c90687306a7244b79ef891f773ed4cf15019fd1f3df330"}, - {file = "pyzmq-22.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b3f57bee62e36be5c97712de32237c5589caee0d1154c2ad01a888accfae20bc"}, - {file = "pyzmq-22.2.1.tar.gz", hash = "sha256:6d18c76676771fd891ca8e0e68da0bbfb88e30129835c0ade748016adb3b6242"}, + {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd"}, + {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2841997a0d85b998cbafecb4183caf51fd19c4357075dfd33eb7efea57e4c149"}, + {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, + {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, + {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, + {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, + {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, + {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, + {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, + {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, + {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, + {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, + {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, + {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, + {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, + {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, + {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, + {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, + {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, + {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, + {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, + {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f43b4a2e6218371dd4f41e547bd919ceeb6ebf4abf31a7a0669cd11cd91ea973"}, + {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, + {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, + {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, + {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:80e043a89c6cadefd3a0712f8a1322038e819ebe9dbac7eca3bce1721bcb63bf"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1621e7a2af72cced1f6ec8ca8ca91d0f76ac236ab2e8828ac8fe909512d566cb"}, + {file = "pyzmq-22.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115"}, + {file = "pyzmq-22.3.0.tar.gz", hash = "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c"}, ] qtconsole = [ {file = "qtconsole-5.1.1-py3-none-any.whl", hash = "sha256:73994105b0369bb99f4164df4a131010f3c7b33a7b5169c37366358d8744675b"}, {file = "qtconsole-5.1.1.tar.gz", hash = "sha256:bbc34bca14f65535afcb401bc74b752bac955e5313001ba640383f7e5857dc49"}, ] qtpy = [ - {file = "QtPy-1.11.1-py2.py3-none-any.whl", hash = "sha256:78f48d7cee7848f92c49ab998f63ca932fddee4b1f89707d6b73eeb0a7110324"}, - {file = "QtPy-1.11.1.tar.gz", hash = "sha256:d471fcb9cf96315b564ad3b42ca830d0286d1049d6a44c578d3dc3836381bb91"}, + {file = "QtPy-1.11.2-py2.py3-none-any.whl", hash = "sha256:83c502973e9fdd7b648d8267a421229ea3d9a0651c22e4c65a4d9228479c39b6"}, + {file = "QtPy-1.11.2.tar.gz", hash = "sha256:d6e4ae3a41f1fcb19762b58f35ad6dd443b4bdc867a4cb81ef10ccd85403c92b"}, ] regex = [ - {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, - {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, - {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, - {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, - {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, - {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, - {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, - {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, - {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, - {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, - {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, - {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, - {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, - {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, - {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, - {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, - {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, - {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, - {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, - {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, - {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, + {file = "regex-2021.9.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66696c8336a1b5d1182464f3af3427cc760118f26d0b09a2ddc16a976a4d2637"}, + {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d87459ad3ab40cd8493774f8a454b2e490d8e729e7e402a0625867a983e4e02"}, + {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf6a1e023caf5e9a982f5377414e1aeac55198831b852835732cfd0a0ca5ff"}, + {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:255791523f80ea8e48e79af7120b4697ef3b74f6886995dcdb08c41f8e516be0"}, + {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e502f8d4e5ef714bcc2c94d499684890c94239526d61fdf1096547db91ca6aa6"}, + {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4907fb0f9b9309a5bded72343e675a252c2589a41871874feace9a05a540241e"}, + {file = "regex-2021.9.30-cp310-cp310-win32.whl", hash = "sha256:3be40f720af170a6b20ddd2ad7904c58b13d2b56f6734ee5d09bbdeed2fa4816"}, + {file = "regex-2021.9.30-cp310-cp310-win_amd64.whl", hash = "sha256:c2b180ed30856dfa70cfe927b0fd38e6b68198a03039abdbeb1f2029758d87e7"}, + {file = "regex-2021.9.30-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6f2d2f93001801296fe3ca86515eb04915472b5380d4d8752f09f25f0b9b0ed"}, + {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fa7ba9ab2eba7284e0d7d94f61df7af86015b0398e123331362270d71fab0b9"}, + {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28040e89a04b60d579c69095c509a4f6a1a5379cd865258e3a186b7105de72c6"}, + {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f588209d3e4797882cd238195c175290dbc501973b10a581086b5c6bcd095ffb"}, + {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42952d325439ef223e4e9db7ee6d9087b5c68c5c15b1f9de68e990837682fc7b"}, + {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cae4099031d80703954c39680323dabd87a69b21262303160776aa0e55970ca0"}, + {file = "regex-2021.9.30-cp36-cp36m-win32.whl", hash = "sha256:0de8ad66b08c3e673b61981b9e3626f8784d5564f8c3928e2ad408c0eb5ac38c"}, + {file = "regex-2021.9.30-cp36-cp36m-win_amd64.whl", hash = "sha256:b345ecde37c86dd7084c62954468a4a655fd2d24fd9b237949dd07a4d0dd6f4c"}, + {file = "regex-2021.9.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6f08187136f11e430638c2c66e1db091105d7c2e9902489f0dbc69b44c222b4"}, + {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b55442650f541d195a535ccec33078c78a9521973fb960923da7515e9ed78fa6"}, + {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87e9c489aa98f50f367fb26cc9c8908d668e9228d327644d7aa568d47e456f47"}, + {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2cb7d4909ed16ed35729d38af585673f1f0833e73dfdf0c18e5be0061107b99"}, + {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0861e7f6325e821d5c40514c551fd538b292f8cc3960086e73491b9c5d8291d"}, + {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:81fdc90f999b2147fc62e303440c424c47e5573a9b615ed5d43a5b832efcca9e"}, + {file = "regex-2021.9.30-cp37-cp37m-win32.whl", hash = "sha256:8c1ad61fa024195136a6b7b89538030bd00df15f90ac177ca278df9b2386c96f"}, + {file = "regex-2021.9.30-cp37-cp37m-win_amd64.whl", hash = "sha256:e3770781353a4886b68ef10cec31c1f61e8e3a0be5f213c2bb15a86efd999bc4"}, + {file = "regex-2021.9.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c065d95a514a06b92a5026766d72ac91bfabf581adb5b29bc5c91d4b3ee9b83"}, + {file = "regex-2021.9.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9925985be05d54b3d25fd6c1ea8e50ff1f7c2744c75bdc4d3b45c790afa2bcb3"}, + {file = "regex-2021.9.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470f2c882f2672d8eeda8ab27992aec277c067d280b52541357e1acd7e606dae"}, + {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad0517df22a97f1da20d8f1c8cb71a5d1997fa383326b81f9cf22c9dadfbdf34"}, + {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e30838df7bfd20db6466fd309d9b580d32855f8e2c2e6d74cf9da27dcd9b63"}, + {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b34d2335d6aedec7dcadd3f8283b9682fadad8b9b008da8788d2fce76125ebe"}, + {file = "regex-2021.9.30-cp38-cp38-win32.whl", hash = "sha256:e07049cece3462c626d650e8bf42ddbca3abf4aa08155002c28cb6d9a5a281e2"}, + {file = "regex-2021.9.30-cp38-cp38-win_amd64.whl", hash = "sha256:37868075eda024470bd0feab872c692ac4ee29db1e14baec103257bf6cc64346"}, + {file = "regex-2021.9.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d331f238a7accfbbe1c4cd1ba610d4c087b206353539331e32a8f05345c74aec"}, + {file = "regex-2021.9.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6348a7ab2a502cbdd0b7fd0496d614007489adb7361956b38044d1d588e66e04"}, + {file = "regex-2021.9.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b1cca6c23f19bee8dc40228d9c314d86d1e51996b86f924aca302fc8f8bf9"}, + {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f1125bc5172ab3a049bc6f4b9c0aae95a2a2001a77e6d6e4239fa3653e202b5"}, + {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:638e98d069b14113e8afba6a54d1ca123f712c0d105e67c1f9211b2a825ef926"}, + {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a0b0db6b49da7fa37ca8eddf9f40a8dbc599bad43e64f452284f37b6c34d91c"}, + {file = "regex-2021.9.30-cp39-cp39-win32.whl", hash = "sha256:9910869c472e5a6728680ca357b5846546cbbd2ab3ad5bef986ef0bc438d0aa6"}, + {file = "regex-2021.9.30-cp39-cp39-win_amd64.whl", hash = "sha256:3b71213ec3bad9a5a02e049f2ec86b3d7c3e350129ae0f4e2f99c12b5da919ed"}, + {file = "regex-2021.9.30.tar.gz", hash = "sha256:81e125d9ba54c34579e4539a967e976a3c56150796674aec318b1b2f49251be7"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, @@ -2625,8 +2646,8 @@ tornado = [ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] tqdm = [ - {file = "tqdm-4.62.2-py2.py3-none-any.whl", hash = "sha256:80aead664e6c1672c4ae20dc50e1cdc5e20eeff9b14aa23ecd426375b28be588"}, - {file = "tqdm-4.62.2.tar.gz", hash = "sha256:a4d6d112e507ef98513ac119ead1159d286deab17dffedd96921412c2d236ff5"}, + {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, + {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, ] traitlets = [ {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, @@ -2638,8 +2659,8 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] urllib3 = [ - {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, - {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, diff --git a/pyproject.toml b/pyproject.toml index bde89303c..c00b03639 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,35 +9,35 @@ packages = [ [tool.poetry.dependencies] python = ">=3.8,<3.9" -networkx = "^2.6.1" -matplotlib = "^3.4.2" -numpy = "^1.21.1" +networkx = "^2.6.3" +matplotlib = "^3.4.3" +numpy = "^1.21.2" pygraphviz = "^1.7" -Pillow = "^8.3.1" +Pillow = "^8.3.2" loguru = "^0.5.3" [tool.poetry.dev-dependencies] -isort = "^5.9.2" -black = "21.7b0" -pylint = "^2.9.3" -pytest = "^6.2.4" -pytest-cov = "^2.12.1" -diff-cover = "^6.2.0" +isort = "^5.9.3" +black = "^21.9b0" +pylint = "^2.11.1" +pytest = "^6.2.5" +pytest-cov = "^3.0.0" +diff-cover = "^6.4.1" mypy = "^0.910" pydocstyle = "^6.1.1" jupyter = "^1.0.0" -nbmake = "^0.5" flake8 = "^3.9.2" -flake8-bugbear = "^21.4.3" -Sphinx = "^4.1.1" +flake8-bugbear = "^21.9.2" +Sphinx = "^4.2.0" sphinx-rtd-theme = "^1.0.0" -myst-parser = "^0.15.1" +myst-parser = "^0.15.2" nbsphinx = "^0.8.7" -tqdm = "^4.62.2" +tqdm = "^4.62.3" psutil = "^5.8.0" py-cpuinfo = "^8.0.0" python-dotenv = "^0.19.0" sphinx-copybutton = "^0.4.0" +nbmake = "^0.9" [build-system] requires = ["poetry-core>=1.0.0"] @@ -47,5 +47,3 @@ build-backend = "poetry.core.masonry.api" filterwarnings = [ "error" ] - - From 61458e89b4bccff7a21c61a86b7f8213d01d033f Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 5 Oct 2021 15:11:48 +0300 Subject: [PATCH 0364/1104] fix(debugging): improve printing of constants in the operation graph --- concrete/common/debugging/printing.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index f82fb8203..0bc093b70 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -52,7 +52,11 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: if isinstance(node, Input): what_to_print = node.input_name elif isinstance(node, Constant): - what_to_print = f"Constant({node.constant_data})" + content = str(node.constant_data).replace("\n", "") + # if content is longer than 25 chars, only show the first and the last 10 chars of it + # 25 is selected using the spaces available before data type information + to_show = f"{content[:10]} ... {content[-10:]}" if len(content) > 25 else content + what_to_print = f"Constant({to_show})" else: base_name = node.__class__.__name__ From ceb23f93d5fdee9c9537e027c5b970c21a5e4cbf Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 5 Oct 2021 15:11:51 +0300 Subject: [PATCH 0365/1104] feat(tracing): implement and test tracing of basic operations for tensors --- concrete/numpy/tracing.py | 29 +++++++++++++ tests/numpy/test_tracing.py | 87 +++++++++++++++++++++++++++++-------- 2 files changed, 99 insertions(+), 17 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index ddcb55702..97246d612 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -304,6 +304,35 @@ def _get_fun(function: numpy.ufunc): # We are populating NPTracer.UFUNC_ROUTING dynamically NPTracer.UFUNC_ROUTING = {fun: _get_fun(fun) for fun in NPTracer.LIST_OF_SUPPORTED_UFUNC} +# We are adding initial support for `np.array(...)` +,-,* `BaseTracer` +# (note that this is not the proper complete handling of these functions) + + +def _on_numpy_add(lhs, rhs): + if isinstance(lhs, BaseTracer): + return lhs.__add__(rhs) + + return rhs.__radd__(lhs) + + +def _on_numpy_subtract(lhs, rhs): + if isinstance(lhs, BaseTracer): + return lhs.__sub__(rhs) + + return rhs.__rsub__(lhs) + + +def _on_numpy_multiply(lhs, rhs): + if isinstance(lhs, BaseTracer): + return lhs.__mul__(rhs) + + return rhs.__rmul__(lhs) + + +NPTracer.UFUNC_ROUTING[numpy.add] = _on_numpy_add +NPTracer.UFUNC_ROUTING[numpy.subtract] = _on_numpy_subtract +NPTracer.UFUNC_ROUTING[numpy.multiply] = _on_numpy_multiply + def trace_numpy_function( function_to_trace: Callable, function_parameters: Dict[str, BaseValue] diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 26a93d562..f3df7e072 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -8,6 +8,7 @@ import pytest from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer +from concrete.common.debugging import get_printable_graph from concrete.common.representation import intermediate as ir from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, EncryptedTensor from concrete.numpy import tracing @@ -165,30 +166,82 @@ def test_numpy_tracing_binary_op(operation, x, y, test_helpers): assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) -@pytest.mark.parametrize( - "tensor_constructor", - [ - EncryptedTensor, - ClearTensor, - ], -) -def test_numpy_tracing_tensor_constant(tensor_constructor): - "Test numpy tracing tensor constant" +def test_numpy_tracing_tensors(): + "Test numpy tracing tensors" - def simple_add_tensor(x): - return x + numpy.array([[1, 2], [3, 4]], dtype=numpy.int32) + def all_operations(x): + intermediate = x + numpy.array([[1, 2], [3, 4]]) + intermediate = numpy.array([[5, 6], [7, 8]]) + intermediate + + intermediate = numpy.array([[100, 200], [300, 400]]) - intermediate + intermediate = intermediate - numpy.array([[10, 20], [30, 40]]) + + intermediate = intermediate * numpy.array([[1, 2], [2, 1]]) + intermediate = numpy.array([[2, 1], [1, 2]]) * intermediate + + return intermediate op_graph = tracing.trace_numpy_function( - simple_add_tensor, {"x": tensor_constructor(Integer(32, True), shape=(2, 2))} + all_operations, {"x": EncryptedTensor(Integer(32, True), shape=(2, 2))} ) - constant_inputs = [node for node in op_graph.graph.nodes() if isinstance(node, ir.Constant)] - assert len(constant_inputs) == 1 + expected = """ +%0 = Constant([[2 1] [1 2]]) # ClearTensor, shape=(2, 2)> +%1 = Constant([[1 2] [2 1]]) # ClearTensor, shape=(2, 2)> +%2 = Constant([[10 20] [30 40]]) # ClearTensor, shape=(2, 2)> +%3 = Constant([[100 200] [300 400]]) # ClearTensor, shape=(2, 2)> +%4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> +%5 = x # EncryptedTensor, shape=(2, 2)> +%6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> +%7 = Add(5, 6) # EncryptedTensor, shape=(2, 2)> +%8 = Add(7, 4) # EncryptedTensor, shape=(2, 2)> +%9 = Sub(3, 8) # EncryptedTensor, shape=(2, 2)> +%10 = Sub(9, 2) # EncryptedTensor, shape=(2, 2)> +%11 = Mul(10, 1) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(11, 0) # EncryptedTensor, shape=(2, 2)> +return(%12) +""".lstrip() - constant_input_data = constant_inputs[0].constant_data + assert get_printable_graph(op_graph, show_data_types=True) == expected - assert (constant_input_data == numpy.array([[1, 2], [3, 4]], dtype=numpy.int32)).all() - assert op_graph.get_ordered_outputs()[0].outputs[0].shape == constant_input_data.shape + +def test_numpy_explicit_tracing_tensors(): + "Test numpy tracing tensors using explicit operations" + + def all_explicit_operations(x): + intermediate = numpy.add(x, numpy.array([[1, 2], [3, 4]])) + intermediate = numpy.add(numpy.array([[5, 6], [7, 8]]), intermediate) + + intermediate = numpy.subtract(numpy.array([[100, 200], [300, 400]]), intermediate) + intermediate = numpy.subtract(intermediate, numpy.array([[10, 20], [30, 40]])) + + intermediate = numpy.multiply(intermediate, numpy.array([[1, 2], [2, 1]])) + intermediate = numpy.multiply(numpy.array([[2, 1], [1, 2]]), intermediate) + + return intermediate + + op_graph = tracing.trace_numpy_function( + all_explicit_operations, {"x": EncryptedTensor(Integer(32, True), shape=(2, 2))} + ) + + expected = """ +%0 = Constant([[2 1] [1 2]]) # ClearTensor, shape=(2, 2)> +%1 = Constant([[1 2] [2 1]]) # ClearTensor, shape=(2, 2)> +%2 = Constant([[10 20] [30 40]]) # ClearTensor, shape=(2, 2)> +%3 = Constant([[100 200] [300 400]]) # ClearTensor, shape=(2, 2)> +%4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> +%5 = x # EncryptedTensor, shape=(2, 2)> +%6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> +%7 = Add(5, 6) # EncryptedTensor, shape=(2, 2)> +%8 = Add(7, 4) # EncryptedTensor, shape=(2, 2)> +%9 = Sub(3, 8) # EncryptedTensor, shape=(2, 2)> +%10 = Sub(9, 2) # EncryptedTensor, shape=(2, 2)> +%11 = Mul(10, 1) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(11, 0) # EncryptedTensor, shape=(2, 2)> +return(%12) +""".lstrip() + + assert get_printable_graph(op_graph, show_data_types=True) == expected @pytest.mark.parametrize( From 83ea485fe1959ea3097e3ddbff99798ef049faa9 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 5 Oct 2021 17:13:47 +0300 Subject: [PATCH 0366/1104] feat(tracing): enable implicit broadcasting for binary operations --- concrete/common/data_types/dtypes_helpers.py | 48 ++++++++++++--- .../common/data_types/test_dtypes_helpers.py | 61 +++++++++++++++++++ tests/numpy/test_tracing.py | 46 ++++++++++++++ 3 files changed, 148 insertions(+), 7 deletions(-) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 00482d79e..87cf75988 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial -from typing import Callable, Union, cast +from typing import Callable, Optional, Tuple, Union, cast from ..debugging.custom_assert import custom_assert from ..values import BaseValue, ClearTensor, EncryptedTensor, TensorValue @@ -212,21 +212,21 @@ def mix_tensor_values_determine_holding_dtype( isinstance(value2, TensorValue), f"Unsupported value2: {value2}, expected TensorValue" ) + resulting_shape = broadcast_shapes(value1.shape, value2.shape) custom_assert( - value1.shape == value2.shape, + resulting_shape is not None, ( - f"Tensors have different shapes which is not supported.\n" + f"Tensors have incompatible shapes which is not supported.\n" f"value1: {value1.shape}, value2: {value2.shape}" ), ) + assert resulting_shape is not None # this is to make mypy happy holding_type = find_type_to_hold_both_lossy(value1.dtype, value2.dtype) - shape = value1.shape - if value1.is_encrypted or value2.is_encrypted: - mixed_value = EncryptedTensor(dtype=holding_type, shape=shape) + mixed_value = EncryptedTensor(dtype=holding_type, shape=resulting_shape) else: - mixed_value = ClearTensor(dtype=holding_type, shape=shape) + mixed_value = ClearTensor(dtype=holding_type, shape=resulting_shape) return mixed_value @@ -344,3 +344,37 @@ def is_data_type_compatible_with( combination = find_type_to_hold_both_lossy(dtype, other) return other == combination + + +def broadcast_shapes(shape1: Tuple[int, ...], shape2: Tuple[int, ...]) -> Optional[Tuple[int, ...]]: + """Broadcast two shapes into a single shape. + + We are mimicing the exact semantics of broadcasting in numpy. + You can learn more about it here: https://numpy.org/doc/stable/user/theory.broadcasting.html + + Args: + shape1 (Tuple[int, ...]): first shape to broadcast + shape2 (Tuple[int, ...]): second shape to broadcast + + Returns: + Optional[Tuple[int, ...]]: None if the shapes are not broadcastable else broadcasted shape + """ + + result = [] + for size1, size2 in zip(shape1[::-1], shape2[::-1]): + if size1 != size2 and size1 != 1 and size2 != 1 and size1 != 0 and size2 != 0: + return None + + if size1 == 0 or size2 == 0: + result.append(0) + else: + result.append(max(size1, size2)) + + if len(result) < len(shape1): + for i in reversed(range(len(shape1) - len(result))): + result.append(shape1[i]) + elif len(result) < len(shape2): + for i in reversed(range(len(shape2) - len(result))): + result.append(shape2[i]) + + return tuple(reversed(result)) diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index 5a0d52c9a..d853d2b26 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -3,6 +3,7 @@ import pytest from concrete.common.data_types.base import BaseDataType from concrete.common.data_types.dtypes_helpers import ( + broadcast_shapes, find_type_to_hold_both_lossy, mix_values_determine_holding_dtype, value_is_encrypted_scalar_integer, @@ -236,3 +237,63 @@ def test_fail_mix_values_determine_holding_dtype(): DummyValue(Integer(32, True), True), DummyValue(Integer(32, True), True), ) + + +@pytest.mark.parametrize( + "shape1,shape2,expected_shape", + [ + pytest.param((), (), ()), + pytest.param((3,), (), (3,)), + pytest.param((3,), (1,), (3,)), + pytest.param((3,), (2,), None), + pytest.param((3,), (3,), (3,)), + pytest.param((2, 3), (), (2, 3)), + pytest.param((2, 3), (1,), (2, 3)), + pytest.param((2, 3), (2,), None), + pytest.param((2, 3), (3,), (2, 3)), + pytest.param((2, 3), (1, 1), (2, 3)), + pytest.param((2, 3), (2, 1), (2, 3)), + pytest.param((2, 3), (3, 1), None), + pytest.param((2, 3), (1, 2), None), + pytest.param((2, 3), (2, 2), None), + pytest.param((2, 3), (3, 2), None), + pytest.param((2, 3), (1, 3), (2, 3)), + pytest.param((2, 3), (2, 3), (2, 3)), + pytest.param((2, 3), (3, 3), None), + pytest.param((2, 1, 3), (1, 1, 1), (2, 1, 3)), + pytest.param((2, 1, 3), (1, 4, 1), (2, 4, 3)), + pytest.param((2, 1, 3), (2, 4, 3), (2, 4, 3)), + # Tests cases taken from `numpy` + # https://github.com/numpy/numpy/blob/623bc1fae1d47df24e7f1e29321d0c0ba2771ce0/numpy/lib/tests/test_stride_tricks.py#L296-L351 + pytest.param((1, 2), (2,), (1, 2)), + pytest.param((1, 1), (3, 4), (3, 4)), + pytest.param((1, 3), (3, 1), (3, 3)), + pytest.param((1, 0), (0, 0), (0, 0)), + pytest.param((0, 1), (0, 0), (0, 0)), + pytest.param((1, 0), (0, 1), (0, 0)), + pytest.param((1, 1), (0, 0), (0, 0)), + pytest.param((1, 1), (1, 0), (1, 0)), + pytest.param((1, 1), (0, 1), (0, 1)), + pytest.param((), (0,), (0,)), + pytest.param((0,), (0, 0), (0, 0)), + pytest.param((0,), (0, 1), (0, 0)), + pytest.param((1,), (0, 0), (0, 0)), + pytest.param((2,), (0, 0), (0, 0)), + pytest.param((), (0, 0), (0, 0)), + pytest.param((1, 1), (0,), (1, 0)), + pytest.param((1,), (0, 1), (0, 1)), + pytest.param((1,), (1, 0), (1, 0)), + pytest.param((), (1, 0), (1, 0)), + pytest.param((), (0, 1), (0, 1)), + pytest.param((1,), (3,), (3,)), + pytest.param((2,), (3, 2), (3, 2)), + pytest.param((3,), (4,), None), + pytest.param((2, 3), (2,), None), + pytest.param((1, 3, 4), (2, 3, 3), None), + pytest.param((2,), (2, 3), None), + ], +) +def test_broadcast_shapes(shape1, shape2, expected_shape): + """Test function for `broadcast_shapes` helper""" + assert broadcast_shapes(shape1=shape1, shape2=shape2) == expected_shape + assert broadcast_shapes(shape1=shape2, shape2=shape1) == expected_shape diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index f3df7e072..9560622df 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -6,6 +6,7 @@ import networkx as nx import numpy import pytest +from concrete.common.data_types.dtypes_helpers import broadcast_shapes from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer from concrete.common.debugging import get_printable_graph @@ -244,6 +245,51 @@ return(%12) assert get_printable_graph(op_graph, show_data_types=True) == expected +@pytest.mark.parametrize( + "x_shape,y_shape", + [ + pytest.param((), ()), + pytest.param((3,), ()), + pytest.param((3,), (1,)), + pytest.param((3,), (2,), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((3,), (3,)), + pytest.param((2, 3), ()), + pytest.param((2, 3), (1,)), + pytest.param((2, 3), (2,), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((2, 3), (3,)), + pytest.param((2, 3), (1, 1)), + pytest.param((2, 3), (2, 1)), + pytest.param((2, 3), (3, 1), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((2, 3), (1, 2), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((2, 3), (2, 2), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((2, 3), (3, 2), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((2, 3), (1, 3)), + pytest.param((2, 3), (2, 3)), + pytest.param((2, 3), (3, 3), marks=pytest.mark.xfail(raises=AssertionError, strict=True)), + pytest.param((2, 1, 3), (1, 1, 1)), + pytest.param((2, 1, 3), (1, 4, 1)), + pytest.param((2, 1, 3), (2, 4, 3)), + ], +) +def test_numpy_tracing_broadcasted_tensors(x_shape, y_shape): + """Test numpy tracing broadcasted tensors""" + + def f(x, y): + return x + y + + op_graph = tracing.trace_numpy_function( + f, + { + "x": EncryptedTensor(Integer(3, True), shape=x_shape), + "y": EncryptedTensor(Integer(3, True), shape=y_shape), + }, + ) + + assert op_graph.input_nodes[0].outputs[0].shape == x_shape + assert op_graph.input_nodes[1].outputs[0].shape == y_shape + assert op_graph.output_nodes[0].outputs[0].shape == broadcast_shapes(x_shape, y_shape) + + @pytest.mark.parametrize( "function_to_trace,op_graph_expected_output_type,input_and_expected_output_tuples", [ From 56e0ed4a11138f6da407e286fcdf51450c4cc755 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 5 Oct 2021 17:12:21 +0200 Subject: [PATCH 0367/1104] feat: manage binary op where one input is constant feat #126 --- concrete/numpy/tracing.py | 103 ++++++++++--- .../common/optimization/test_float_fusing.py | 142 ++++++++++++++---- tests/numpy/test_tracing.py | 73 +++++---- 3 files changed, 232 insertions(+), 86 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 97246d612..fbdc330a9 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -45,7 +45,9 @@ class NPTracer(BaseTracer): (len(kwargs) == 0), f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc}", ) - return tracing_func(*input_tracers, **kwargs) + # Create constant tracers when needed + sanitized_input_tracers = [self._sanitize(inp) for inp in input_tracers] + return tracing_func(*sanitized_input_tracers, **kwargs) raise NotImplementedError("Only __call__ method is supported currently") def __array_function__(self, func, _types, args, kwargs): @@ -163,6 +165,61 @@ class NPTracer(BaseTracer): ) return output_tracer + @classmethod + def _binary_operator( + cls, binary_operator, binary_operator_string, *input_tracers: "NPTracer", **kwargs + ) -> "NPTracer": + """Trace a binary operator, supposing one of the input is a constant. + + If no input is a constant, raises an error. + + Returns: + NPTracer: The output NPTracer containing the traced function + """ + custom_assert(len(input_tracers) == 2) + + # One of the inputs has to be constant + if isinstance(input_tracers[0].traced_computation, Constant): + in_which_input_is_constant = 0 + baked_constant = deepcopy(input_tracers[0].traced_computation.constant_data) + elif isinstance(input_tracers[1].traced_computation, Constant): + in_which_input_is_constant = 1 + baked_constant = deepcopy(input_tracers[1].traced_computation.constant_data) + else: + raise NotImplementedError(f"Can't manage binary operator {binary_operator}") + + in_which_input_is_variable = 1 - in_which_input_is_constant + + if in_which_input_is_constant == 0: + + def arbitrary_func(x, baked_constant, **kwargs): + return binary_operator(baked_constant, x, **kwargs) + + else: + + def arbitrary_func(x, baked_constant, **kwargs): + return binary_operator(x, baked_constant, **kwargs) + + common_output_dtypes = cls._manage_dtypes(binary_operator, *input_tracers) + custom_assert(len(common_output_dtypes) == 1) + + op_kwargs = deepcopy(kwargs) + op_kwargs["baked_constant"] = baked_constant + + traced_computation = ArbitraryFunction( + input_base_value=input_tracers[in_which_input_is_variable].output, + arbitrary_func=arbitrary_func, + output_dtype=common_output_dtypes[0], + op_kwargs=op_kwargs, + op_name=binary_operator_string, + ) + output_tracer = cls( + (input_tracers[in_which_input_is_variable],), + traced_computation=traced_computation, + output_index=0, + ) + return output_tracer + def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": """Trace numpy.dot. @@ -188,8 +245,9 @@ class NPTracer(BaseTracer): ) return output_tracer + # Supported functions are either univariate or bivariate for which one of the two + # sources is a constant LIST_OF_SUPPORTED_UFUNC: List[numpy.ufunc] = [ - # The commented functions are functions require more than a single argument numpy.absolute, # numpy.add, numpy.arccos, @@ -197,7 +255,7 @@ class NPTracer(BaseTracer): numpy.arcsin, numpy.arcsinh, numpy.arctan, - # numpy.arctan2, + numpy.arctan2, numpy.arctanh, # numpy.bitwise_and, # numpy.bitwise_or, @@ -216,7 +274,7 @@ class NPTracer(BaseTracer): numpy.exp2, numpy.expm1, numpy.fabs, - # numpy.float_power, + numpy.float_power, numpy.floor, # numpy.floor_divide, # numpy.fmax, @@ -289,7 +347,7 @@ class NPTracer(BaseTracer): } -def _get_fun(function: numpy.ufunc): +def _get_unary_fun(function: numpy.ufunc): """Wrap _unary_operator in a lambda to populate NPTRACER.UFUNC_ROUTING.""" # We have to access this method to be able to build NPTracer.UFUNC_ROUTING @@ -301,32 +359,41 @@ def _get_fun(function: numpy.ufunc): # pylint: enable=protected-access +def _get_binary_fun(function: numpy.ufunc): + """Wrap _binary_operator in a lambda to populate NPTRACER.UFUNC_ROUTING.""" + + # We have to access this method to be able to build NPTracer.UFUNC_ROUTING + # dynamically + # pylint: disable=protected-access + return lambda *input_tracers, **kwargs: NPTracer._binary_operator( + function, f"np.{function.__name__}", *input_tracers, **kwargs + ) + # pylint: enable=protected-access + + # We are populating NPTracer.UFUNC_ROUTING dynamically -NPTracer.UFUNC_ROUTING = {fun: _get_fun(fun) for fun in NPTracer.LIST_OF_SUPPORTED_UFUNC} +NPTracer.UFUNC_ROUTING = { + fun: _get_unary_fun(fun) for fun in NPTracer.LIST_OF_SUPPORTED_UFUNC if fun.nin == 1 +} + +NPTracer.UFUNC_ROUTING[numpy.arctan2] = _get_binary_fun(numpy.arctan2) +NPTracer.UFUNC_ROUTING[numpy.float_power] = _get_binary_fun(numpy.float_power) + # We are adding initial support for `np.array(...)` +,-,* `BaseTracer` # (note that this is not the proper complete handling of these functions) def _on_numpy_add(lhs, rhs): - if isinstance(lhs, BaseTracer): - return lhs.__add__(rhs) - - return rhs.__radd__(lhs) + return lhs.__add__(rhs) def _on_numpy_subtract(lhs, rhs): - if isinstance(lhs, BaseTracer): - return lhs.__sub__(rhs) - - return rhs.__rsub__(lhs) + return lhs.__sub__(rhs) def _on_numpy_multiply(lhs, rhs): - if isinstance(lhs, BaseTracer): - return lhs.__mul__(rhs) - - return rhs.__rmul__(lhs) + return lhs.__mul__(rhs) NPTracer.UFUNC_ROUTING[numpy.add] = _on_numpy_add diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index f10535aab..7f017ed7a 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -146,45 +146,127 @@ def test_tensor_no_fuse(): assert orig_num_nodes == fused_num_nodes -def test_fuse_float_operations_correctness(): - """Test functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC - with fuse_float_operations.""" +def subtest_fuse_float_unary_operations_correctness(fun): + """Test a unary function with fuse_float_operations.""" - for fun in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: + # Some manipulation to avoid issues with domain of definitions of functions + if fun == numpy.arccosh: + input_list = [1, 2, 42, 44] + super_fun_list = [complex_fuse_direct_input] + elif fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: + input_list = [0, 0.1, 0.2] + super_fun_list = [complex_fuse_direct_input] + else: + input_list = [0, 2, 42, 44] + super_fun_list = [complex_fuse_direct_input, complex_fuse_indirect_input] + + for super_fun in super_fun_list: + + for input_ in input_list: + + def get_function_to_trace(): + return lambda x, y: super_fun(fun, x, y) + + function_to_trace = get_function_to_trace() + + params_names = signature(function_to_trace).parameters.keys() + + op_graph = trace_numpy_function( + function_to_trace, + {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + ) + orig_num_nodes = len(op_graph.graph) + fuse_float_operations(op_graph) + fused_num_nodes = len(op_graph.graph) + + assert fused_num_nodes < orig_num_nodes + + input_ = numpy.int32(input_) + + num_params = len(params_names) + inputs = (input_,) * num_params + + assert function_to_trace(*inputs) == op_graph(*inputs) + + +def subtest_fuse_float_binary_operations_correctness(fun): + """Test a binary functions with fuse_float_operations, with a constant as a source.""" + + for i in range(4): + + # For bivariate functions: fix one of the inputs + if i == 0: + # With an integer in first position + def get_function_to_trace(): + return lambda x, y: fun(3, x + y).astype(numpy.int32) + + elif i == 1: + # With a float in first position + def get_function_to_trace(): + return lambda x, y: fun(2.3, x + y).astype(numpy.int32) + + elif i == 2: + # With an integer in second position + def get_function_to_trace(): + return lambda x, y: fun(x + y, 4).astype(numpy.int32) - if fun == numpy.arccosh: - input_list = [1, 2, 42, 44] - super_fun_list = [complex_fuse_direct_input] - elif fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: - input_list = [0, 0.1, 0.2] - super_fun_list = [complex_fuse_direct_input] else: - input_list = [0, 2, 42, 44] - super_fun_list = [complex_fuse_direct_input, complex_fuse_indirect_input] + # With a float in second position + def get_function_to_trace(): + return lambda x, y: fun(x + y, 5.7).astype(numpy.int32) - for super_fun in super_fun_list: + input_list = [0, 2, 42, 44] - for input_ in input_list: + for input_ in input_list: - def get_function_to_trace(): - return lambda x, y: super_fun(fun, x, y) + function_to_trace = get_function_to_trace() - function_to_trace = get_function_to_trace() + params_names = signature(function_to_trace).parameters.keys() - params_names = signature(function_to_trace).parameters.keys() + op_graph = trace_numpy_function( + function_to_trace, + {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + ) + orig_num_nodes = len(op_graph.graph) + fuse_float_operations(op_graph) + fused_num_nodes = len(op_graph.graph) - op_graph = trace_numpy_function( - function_to_trace, - {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, - ) - orig_num_nodes = len(op_graph.graph) - fuse_float_operations(op_graph) - fused_num_nodes = len(op_graph.graph) + assert fused_num_nodes < orig_num_nodes - assert fused_num_nodes < orig_num_nodes + input_ = numpy.int32(input_) - input_ = numpy.int32(input_) + num_params = len(params_names) + inputs = (input_,) * num_params - num_params = len(params_names) - inputs = (input_,) * num_params - assert function_to_trace(*inputs) == op_graph(*inputs) + assert function_to_trace(*inputs) == op_graph(*inputs) + + +def subtest_fuse_float_binary_operations_dont_support_two_variables(fun): + """Test a binary function with fuse_float_operations, with no constant as + a source.""" + + def get_function_to_trace(): + return lambda x, y: fun(x, y).astype(numpy.int32) + + function_to_trace = get_function_to_trace() + + params_names = signature(function_to_trace).parameters.keys() + + with pytest.raises(NotImplementedError, match=r"Can't manage binary operator"): + trace_numpy_function( + function_to_trace, + {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + ) + + +@pytest.mark.parametrize("fun", tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC) +def test_ufunc_operations(fun): + """Test functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" + + if fun.nin == 1: + subtest_fuse_float_unary_operations_correctness(fun) + elif fun.nin == 2: + subtest_fuse_float_binary_operations_correctness(fun) + subtest_fuse_float_binary_operations_dont_support_two_variables(fun) + else: + raise NotImplementedError("Only unary and binary functions are tested for now") diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 9560622df..6a9998d95 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -195,11 +195,11 @@ def test_numpy_tracing_tensors(): %5 = x # EncryptedTensor, shape=(2, 2)> %6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> %7 = Add(5, 6) # EncryptedTensor, shape=(2, 2)> -%8 = Add(7, 4) # EncryptedTensor, shape=(2, 2)> +%8 = Add(4, 7) # EncryptedTensor, shape=(2, 2)> %9 = Sub(3, 8) # EncryptedTensor, shape=(2, 2)> %10 = Sub(9, 2) # EncryptedTensor, shape=(2, 2)> %11 = Mul(10, 1) # EncryptedTensor, shape=(2, 2)> -%12 = Mul(11, 0) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(0, 11) # EncryptedTensor, shape=(2, 2)> return(%12) """.lstrip() @@ -234,11 +234,11 @@ def test_numpy_explicit_tracing_tensors(): %5 = x # EncryptedTensor, shape=(2, 2)> %6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> %7 = Add(5, 6) # EncryptedTensor, shape=(2, 2)> -%8 = Add(7, 4) # EncryptedTensor, shape=(2, 2)> +%8 = Add(4, 7) # EncryptedTensor, shape=(2, 2)> %9 = Sub(3, 8) # EncryptedTensor, shape=(2, 2)> %10 = Sub(9, 2) # EncryptedTensor, shape=(2, 2)> %11 = Mul(10, 1) # EncryptedTensor, shape=(2, 2)> -%12 = Mul(11, 0) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(0, 11) # EncryptedTensor, shape=(2, 2)> return(%12) """.lstrip() @@ -406,44 +406,43 @@ def test_tracing_astype( ), ], ) -def test_trace_numpy_supported_ufuncs(inputs, expected_output_node): +@pytest.mark.parametrize( + "function_to_trace_def", [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1] +) +def test_trace_numpy_supported_unary_ufuncs(inputs, expected_output_node, function_to_trace_def): """Function to trace supported numpy ufuncs""" - for function_to_trace_def in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: + # We really need a lambda (because numpy functions are not playing + # nice with inspect.signature), but pylint and flake8 are not happy + # with it + # pylint: disable=unnecessary-lambda,cell-var-from-loop + function_to_trace = lambda x: function_to_trace_def(x) # noqa: E731 + # pylint: enable=unnecessary-lambda,cell-var-from-loop - # We really need a lambda (because numpy functions are not playing - # nice with inspect.signature), but pylint and flake8 are not happy - # with it - # pylint: disable=unnecessary-lambda,cell-var-from-loop - function_to_trace = lambda x: function_to_trace_def(x) # noqa: E731 - # pylint: enable=unnecessary-lambda,cell-var-from-loop + op_graph = tracing.trace_numpy_function(function_to_trace, inputs) - op_graph = tracing.trace_numpy_function(function_to_trace, inputs) + assert len(op_graph.output_nodes) == 1 + assert isinstance(op_graph.output_nodes[0], expected_output_node) + assert len(op_graph.output_nodes[0].outputs) == 1 - assert len(op_graph.output_nodes) == 1 - assert isinstance(op_graph.output_nodes[0], expected_output_node) - assert len(op_graph.output_nodes[0].outputs) == 1 + if function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: + assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) + elif function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_BOOL: - if function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_FLOAT64: - assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Float(64)) - elif function_to_trace_def in LIST_OF_UFUNC_WHOSE_OUTPUT_IS_BOOL: + # Boolean function + assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar(Integer(8, is_signed=False)) + else: - # Boolean function - assert op_graph.output_nodes[0].outputs[0] == EncryptedScalar( - Integer(8, is_signed=False) - ) - else: + # Function keeping more or less input type + input_node_type = inputs["x"] - # Function keeping more or less input type - input_node_type = inputs["x"] + expected_output_node_type = deepcopy(input_node_type) - expected_output_node_type = deepcopy(input_node_type) + expected_output_node_type.dtype.bit_width = max( + expected_output_node_type.dtype.bit_width, 32 + ) - expected_output_node_type.dtype.bit_width = max( - expected_output_node_type.dtype.bit_width, 32 - ) - - assert op_graph.output_nodes[0].outputs[0] == expected_output_node_type + assert op_graph.output_nodes[0].outputs[0] == expected_output_node_type def test_trace_numpy_ufuncs_not_supported(): @@ -516,15 +515,13 @@ def test_trace_numpy_dot(function_to_trace, inputs, expected_output_node, expect assert op_graph.output_nodes[0].outputs[0] == expected_output_value -def test_nptracer_get_tracing_func_for_np_functions(): +@pytest.mark.parametrize("np_function", tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC) +def test_nptracer_get_tracing_func_for_np_functions(np_function): """Test NPTracer get_tracing_func_for_np_function""" - for np_function in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC: - expected_tracing_func = tracing.NPTracer.UFUNC_ROUTING[np_function] + expected_tracing_func = tracing.NPTracer.UFUNC_ROUTING[np_function] - assert ( - tracing.NPTracer.get_tracing_func_for_np_function(np_function) == expected_tracing_func - ) + assert tracing.NPTracer.get_tracing_func_for_np_function(np_function) == expected_tracing_func def test_nptracer_get_tracing_func_for_np_functions_not_implemented(): From 05e1227269cd4e479abed48fa7cfdeb976b3df6b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 09:45:14 +0200 Subject: [PATCH 0368/1104] chore: update env docker to have runtime lib path in LD_PRELOAD - update the rest of the dockers, script and CI to stop setting LD_PRELOAD --- .github/workflows/continuous-integration.yaml | 20 +------------------ Makefile | 2 +- docker/Dockerfile.concretefhe-dev | 1 - docker/Dockerfile.concretefhe-env | 2 ++ docker/Dockerfile.release | 7 ++++--- ...enchmark_and_publish_findings_in_docker.sh | 1 - 6 files changed, 8 insertions(+), 25 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 7db611ab1..4799229e5 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -223,20 +223,12 @@ jobs: - name: Source code Conformance id: cs if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} - env: - # TODO: remove this when JIT doesn't need this - # Required to be sure that docs reads all files with MLIR imports properly - LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so # pcc launches an internal target with proper flags run: | make pcc - name: Build docs id: cbd if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} - env: - # TODO: remove this when JIT doesn't need this - # Required to be sure that docs reads all files with MLIR imports properly - LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make docs - name: Conformance status @@ -259,23 +251,14 @@ jobs: - name: PyTest Source Code id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} - env: - # TODO: remove this when JIT doesn't need this - LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make pytest - name: Test CodeBlocks if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} - env: - # TODO: remove this when JIT doesn't need this - LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make test_codeblocks - name: PyTest Notebooks if: ${{ github.event_name == 'schedule' && steps.conformance.outcome == 'success' && !cancelled() }} - env: - # TODO: remove this when JIT doesn't need this - LD_PRELOAD: /compiler/build/lib/Runtime/libZamalangRuntime.so run: | make pytest_nb - name: Test coverage @@ -505,8 +488,7 @@ jobs: if: ${{ success() && !cancelled() }} run: | echo "Running sanity check for ${RELEASE_IMG_GIT_TAG}" - docker run --rm --env LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so \ - -v "$(pwd)"/docker/release_resources:/data \ + docker run --rm -v "$(pwd)"/docker/release_resources:/data \ "${RELEASE_IMG_GIT_TAG}" /bin/bash -c "python ./sanity_check.py" docker image push --all-tags "${RELEASE_IMAGE_BASE}" - name: Set notification report diff --git a/Makefile b/Makefile index 2b60d85bd..feb2c0f4a 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,7 @@ docker_publish_measurements: docker_build @# Thus, we ran `extract_machine_info.py` script using native python python script/progress_tracker_utils/extract_machine_info.py docker run --rm --volume /"$$(pwd)":/src $(DEV_DOCKER_IMG) \ - /bin/bash ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh + /bin/bash ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh .PHONY: docker_publish_measurements docs: clean_docs diff --git a/docker/Dockerfile.concretefhe-dev b/docker/Dockerfile.concretefhe-dev index 9b3a6e1e9..753a087ca 100644 --- a/docker/Dockerfile.concretefhe-dev +++ b/docker/Dockerfile.concretefhe-dev @@ -8,7 +8,6 @@ RUN echo "source /${SRC_DIR_NAME}/.docker_venv/bin/activate" >> /root/.bashrc && echo " source /${SRC_DIR_NAME}/.docker_venv/bin/activate" >> /root/.bashrc && \ echo " cd /${SRC_DIR_NAME}/ && make setup_env" >> /root/.bashrc && \ echo "fi" >> /root/.bashrc && \ - echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc && \ echo "export MPLBACKEND=TkAgg" >> /root/.bashrc WORKDIR /${SRC_DIR_NAME} diff --git a/docker/Dockerfile.concretefhe-env b/docker/Dockerfile.concretefhe-env index f2a126fe6..6bd127f85 100644 --- a/docker/Dockerfile.concretefhe-env +++ b/docker/Dockerfile.concretefhe-env @@ -14,3 +14,5 @@ RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir poetry + +ENV LD_PRELOAD=${RT_LIB}:${LD_PRELOAD} diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index d22952d06..ce5e7abfa 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -1,4 +1,4 @@ -FROM ghcr.io/zama-ai/zamalang-compiler:3a254bcb8725507a11538913d3d5e9657ac00043 as builder +FROM ghcr.io/zama-ai/zamalang-compiler:a9fae4c19b96ee61c7ea0a2ce26b1cd8d049e159 as builder RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ apt-get install --no-install-recommends -y \ @@ -14,7 +14,7 @@ COPY pyproject.toml ./pyproject.toml RUN poetry build --format wheel -FROM ghcr.io/zama-ai/zamalang-compiler:3a254bcb8725507a11538913d3d5e9657ac00043 +FROM ghcr.io/zama-ai/zamalang-compiler:a9fae4c19b96ee61c7ea0a2ce26b1cd8d049e159 RUN mkdir /pkg && mkdir /app WORKDIR /pkg @@ -30,13 +30,14 @@ RUN apt-get update && apt-get upgrade --no-install-recommends -y && \ graphviz* && \ rm -rf /var/lib/apt/lists/* && \ python3 -m pip install --no-cache-dir --upgrade pip wheel setuptools && \ - echo "export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so" >> /root/.bashrc && \ echo "export MPLBACKEND=TkAgg" >> /root/.bashrc && \ python3 -m pip install --no-cache-dir ./*.whl && \ python3 -m pip install --no-cache-dir -r torch_requirements.txt \ -f https://download.pytorch.org/whl/torch_stable.html && \ python3 -m pip install --no-cache-dir -r release_requirements.txt +ENV LD_PRELOAD=${RT_LIB}:${LD_PRELOAD} + WORKDIR /app COPY docker/release_resources/entry_point.sh ./entry_point.sh RUN mkdir /data diff --git a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh index 99c8c4dae..44ffdce4f 100755 --- a/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh +++ b/script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh @@ -12,7 +12,6 @@ if ! source /src/.docker_venv/bin/activate; then source /src/.docker_venv/bin/activate cd /src/ && make setup_env fi -export LD_PRELOAD=/compiler/build/lib/Runtime/libZamalangRuntime.so initial_log=logs/$(date -u --iso-8601=seconds).log From 4a77d0515a92c03ba36a778610d1e858dce8533e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 10:41:44 +0200 Subject: [PATCH 0369/1104] feat(tracing): add test for extra args passed to ufuncs - add comment to explain why sanitizing all args is safe for ufuncs --- concrete/numpy/tracing.py | 12 +++++++----- tests/numpy/test_tracing.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index fbdc330a9..4af5fd446 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -34,7 +34,7 @@ class NPTracer(BaseTracer): _mix_values_func: Callable[..., BaseValue] = mix_values_determine_holding_dtype - def __array_ufunc__(self, ufunc, method, *input_tracers, **kwargs): + def __array_ufunc__(self, ufunc: numpy.ufunc, method, *args, **kwargs): """Catch calls to numpy ufunc and routes them to tracing functions if supported. Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch @@ -43,11 +43,13 @@ class NPTracer(BaseTracer): tracing_func = self.get_tracing_func_for_np_function(ufunc) custom_assert( (len(kwargs) == 0), - f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc}", + f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc.__name__}", ) - # Create constant tracers when needed - sanitized_input_tracers = [self._sanitize(inp) for inp in input_tracers] - return tracing_func(*sanitized_input_tracers, **kwargs) + + # Create constant tracers for args, numpy only passes ufunc.nin args so we can + # sanitize all of them without issues + sanitized_args = [self._sanitize(arg) for arg in args] + return tracing_func(*sanitized_args, **kwargs) raise NotImplementedError("Only __call__ method is supported currently") def __array_function__(self, func, _types, args, kwargs): diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 6a9998d95..dffa405fc 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -462,6 +462,40 @@ def test_trace_numpy_ufuncs_not_supported(): assert "Only __call__ method is supported currently" in str(excinfo.value) +def test_trace_numpy_ufuncs_no_kwargs_no_extra_args(): + """Test a case where kwargs are not allowed and too many inputs are passed""" + inputs = { + "x": EncryptedScalar(Integer(32, is_signed=True)), + "y": EncryptedScalar(Integer(32, is_signed=True)), + "z": EncryptedScalar(Integer(32, is_signed=True)), + } + + # We really need a lambda (because numpy functions are not playing + # nice with inspect.signature), but pylint and flake8 are not happy + # with it + # pylint: disable=unnecessary-lambda + function_to_trace = lambda x, y, z: numpy.add(x, y, z) # noqa: E731 + # pylint: enable=unnecessary-lambda + + with pytest.raises(AssertionError) as excinfo: + tracing.trace_numpy_function(function_to_trace, inputs) + + # numpy only passes ufunc.nin tracers so the extra arguments are passed as kwargs + assert "**kwargs are currently not supported for numpy ufuncs, ufunc: add" in str(excinfo.value) + + # We really need a lambda (because numpy functions are not playing + # nice with inspect.signature), but pylint and flake8 are not happy + # with it + # pylint: disable=unnecessary-lambda + function_to_trace = lambda x, y, z: numpy.add(x, y, out=z) # noqa: E731 + # pylint: enable=unnecessary-lambda + + with pytest.raises(AssertionError) as excinfo: + tracing.trace_numpy_function(function_to_trace, inputs) + + assert "**kwargs are currently not supported for numpy ufuncs, ufunc: add" in str(excinfo.value) + + @pytest.mark.parametrize( "function_to_trace,inputs,expected_output_node,expected_output_value", [ From 0317dd49ea41aa9c467c3e7842079dbfad6510c1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 12:39:03 +0200 Subject: [PATCH 0370/1104] feat: manage sanitization properly for numpy functions - allows to use dot with constant inputs --- concrete/numpy/tracing.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 4af5fd446..5cd78bc4f 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -7,7 +7,7 @@ import numpy from numpy.typing import DTypeLike from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype -from ..common.debugging.custom_assert import custom_assert +from ..common.debugging.custom_assert import assert_true, custom_assert from ..common.operator_graph import OPGraph from ..common.representation.intermediate import ArbitraryFunction, Constant, Dot from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters @@ -53,7 +53,7 @@ class NPTracer(BaseTracer): raise NotImplementedError("Only __call__ method is supported currently") def __array_function__(self, func, _types, args, kwargs): - """Catch calls to numpy function in routes them to hnp functions if supported. + """Catch calls to numpy function in routes them to tracing functions if supported. Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch """ @@ -62,7 +62,8 @@ class NPTracer(BaseTracer): (len(kwargs) == 0), f"**kwargs are currently not supported for numpy functions, func: {func}", ) - return tracing_func(*args, **kwargs) + sanitized_args = [self._sanitize(arg) for arg in args] + return tracing_func(self, *sanitized_args, **kwargs) def astype(self, numpy_dtype: DTypeLike, *args, **kwargs) -> "NPTracer": r"""Support numpy astype feature. @@ -222,26 +223,25 @@ class NPTracer(BaseTracer): ) return output_tracer - def dot(self, other_tracer: "NPTracer", **_kwargs) -> "NPTracer": + def dot(self, *args: "NPTracer", **_kwargs) -> "NPTracer": """Trace numpy.dot. Returns: NPTracer: The output NPTracer containing the traced function """ - # input_tracers contains the other tracer of the dot product - dot_inputs = (self, self._sanitize(other_tracer)) + assert_true((num_args := len(args)) == 2, f"dot expects 2 inputs got {num_args}") - common_output_dtypes = self._manage_dtypes(numpy.dot, *dot_inputs) + common_output_dtypes = self._manage_dtypes(numpy.dot, *args) custom_assert(len(common_output_dtypes) == 1) traced_computation = Dot( - [input_tracer.output for input_tracer in dot_inputs], + [input_tracer.output for input_tracer in args], common_output_dtypes[0], delegate_evaluation_function=numpy.dot, ) output_tracer = self.__class__( - dot_inputs, + args, traced_computation=traced_computation, output_index=0, ) From 674b86cf62ee1605b59ec1f4791a4939147c2653 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 14:48:14 +0200 Subject: [PATCH 0371/1104] chore: do not fail on PR coverage comment - had the case for whatever reason. Let's harden our workflow some more --- .github/workflows/continuous-integration.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 4799229e5..2241299cd 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -275,6 +275,7 @@ jobs: - name: Comment with coverage uses: marocchino/sticky-pull-request-comment@82e7a0d3c51217201b3fedc4ddde6632e969a477 if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} + continue-on-error: true with: path: diff-coverage.txt recreate: true From 5fce0d2920d450d1c22cfb2824b1aa1e43e61961 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 6 Oct 2021 16:47:16 +0300 Subject: [PATCH 0372/1104] feat(compilation): implement MLIR conversion of constant arrays --- concrete/common/mlir/converters.py | 51 +++++++++++--- tests/common/mlir/test_converters.py | 9 ++- tests/common/mlir/test_mlir_converter.py | 30 ++++++++ tests/numpy/test_compile.py | 87 ++++++++++++++++++++---- 4 files changed, 152 insertions(+), 25 deletions(-) diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 9cb12213a..aae80ad1b 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -11,18 +11,19 @@ from typing import cast # pylint: disable=no-name-in-module,no-member import numpy as np from mlir.dialects import std as std_dialect -from mlir.ir import DenseElementsAttr, IntegerAttr, IntegerType, RankedTensorType +from mlir.ir import Attribute, DenseElementsAttr, IntegerAttr, IntegerType, RankedTensorType from zamalang.dialects import hlfhe -from ...common.data_types.integers import Integer from ..data_types.dtypes_helpers import ( value_is_clear_scalar_integer, value_is_clear_tensor_integer, value_is_encrypted_scalar_unsigned_integer, value_is_encrypted_tensor_integer, ) +from ..data_types.integers import Integer from ..debugging.custom_assert import custom_assert from ..representation.intermediate import Add, ArbitraryFunction, Constant, Dot, Mul, Sub +from ..values import TensorValue def add(node, preds, ir_to_mlir_node, ctx): @@ -123,14 +124,44 @@ def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): def constant(node, _, __, ctx): - """Convert a constant inputs.""" - if not value_is_clear_scalar_integer(node.outputs[0]): - raise TypeError("Don't support non-integer constants") - dtype = cast(Integer, node.outputs[0].dtype) - if dtype.is_signed: - raise TypeError("Don't support signed constant integer") - int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) - return std_dialect.ConstantOp(int_type, IntegerAttr.get(int_type, node.constant_data)).result + """Convert a constant input.""" + value = node.outputs[0] + + if value_is_clear_scalar_integer(value): + value = cast(TensorValue, value) + + dtype = cast(Integer, value.dtype) + if dtype.is_signed: + raise TypeError("Don't support signed constant integer") + data = node.constant_data + + int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) + return std_dialect.ConstantOp(int_type, IntegerAttr.get(int_type, data)).result + + if value_is_clear_tensor_integer(value): + value = cast(TensorValue, value) + + dtype = cast(Integer, value.dtype) + if dtype.is_signed: + raise TypeError("Don't support signed constant integer tensor") + data = node.constant_data + + int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) + vec_type = RankedTensorType.get(value.shape, int_type) + + # usage of `Attribute.parse` is the result of some limitations in the MLIR module + # provided by LLVM + + # `DenseElementsAttr` should have been used instead but it's impossible to assign + # custom bit-widths using it (e.g., uint5) + + # since we coudn't create a `DenseElementsAttr` with a custom bit width using python api + # we use `Attribute.parse` to let the underlying library do it by itself + + value_attr = Attribute.parse(f"dense<{str(data.tolist())}> : {vec_type}") + return std_dialect.ConstantOp(vec_type, value_attr).result + + raise TypeError(f"Don't support {value} constants") def apply_lut(node, preds, ir_to_mlir_node, ctx): diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index 0f00e6c6c..89cfdfd26 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -4,7 +4,7 @@ import pytest from concrete.common.data_types.floats import Float from concrete.common.data_types.integers import Integer from concrete.common.mlir.converters import add, apply_lut, constant, dot, mul, sub -from concrete.common.values import ClearScalar, EncryptedScalar +from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar class MockNode: @@ -30,14 +30,19 @@ def test_failing_converter(converter): def test_fail_non_integer_const(): """Test failing constant converter with non-integer""" - with pytest.raises(TypeError, match=r"Don't support non-integer constants"): + with pytest.raises(TypeError, match=r"Don't support .* constants"): constant(MockNode(outputs=[ClearScalar(Float(32))]), None, None, None) + with pytest.raises(TypeError, match=r"Don't support .* constants"): + constant(MockNode(outputs=[ClearTensor(Float(32), shape=(2,))]), None, None, None) + def test_fail_signed_integer_const(): """Test failing constant converter with non-integer""" with pytest.raises(TypeError, match=r"Don't support signed constant integer"): constant(MockNode(outputs=[ClearScalar(Integer(8, True))]), None, None, None) + with pytest.raises(TypeError, match=r"Don't support signed constant integer tensor"): + constant(MockNode(outputs=[ClearTensor(Integer(8, True), shape=(2,))]), None, None, None) @pytest.mark.parametrize( diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 305d879ca..cfb51c91e 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -267,6 +267,36 @@ def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): compiler.round_trip(mlir_result) +def test_mlir_converter_dot_vector_and_constant(): + """Test the conversion to MLIR by calling the parser from the compiler""" + + def left_dot_with_constant(x): + return numpy.dot(x, numpy.array([1, 2])) + + def right_dot_with_constant(x): + return numpy.dot(numpy.array([1, 2]), x) + + left_graph = compile_numpy_function_into_op_graph( + left_dot_with_constant, + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2,))}, + [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], + ) + left_converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + left_mlir = left_converter.convert(left_graph) + + right_graph = compile_numpy_function_into_op_graph( + right_dot_with_constant, + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2,))}, + [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], + ) + right_converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + right_mlir = right_converter.convert(right_graph) + + # testing that this doesn't raise an error + compiler.round_trip(left_mlir) + compiler.round_trip(right_mlir) + + def test_concrete_encrypted_integer_to_mlir_type(): """Test conversion of EncryptedScalar into MLIR""" value = EncryptedScalar(Integer(7, is_signed=False)) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 8f141ae90..afa6bb3e1 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -161,11 +161,11 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): (0, 5), ), pytest.param( - 8, + 6, (0, 4), ), pytest.param( - 16, + 10, (0, 3), ), ], @@ -173,18 +173,22 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): def test_compile_and_run_dot_correctness(size, input_range): """Test correctness of results when running a compiled function""" - def data_gen(input_range, size): - for _ in range(1000): - low, high = input_range - args = [ - numpy.array([random.randint(low, high) for _ in range(size)]) for __ in range(2) - ] + low, high = input_range + shape = (size,) - yield args + inputset = [ + (numpy.zeros(shape, dtype=numpy.uint32), numpy.zeros(shape, dtype=numpy.uint32)), + ( + numpy.ones(shape, dtype=numpy.uint32) * high, + numpy.ones(shape, dtype=numpy.uint32) * high, + ), + ] + for _ in range(8): + inputset.append((numpy.random.randint(low, high + 1), numpy.random.randint(low, high + 1))) function_parameters = { - "x": EncryptedTensor(Integer(64, False), (size,)), - "y": ClearTensor(Integer(64, False), (size,)), + "x": EncryptedTensor(Integer(64, False), shape), + "y": ClearTensor(Integer(64, False), shape), } def function(x, y): @@ -193,14 +197,71 @@ def test_compile_and_run_dot_correctness(size, input_range): compiler_engine = compile_numpy_function( function, function_parameters, - data_gen(input_range, size), + inputset, ) - low, high = input_range args = [[random.randint(low, high) for _ in range(size)] for __ in range(2)] assert compiler_engine.run(*args) == function(*args) +@pytest.mark.parametrize( + "size,input_range", + [ + pytest.param( + 1, + (0, 8), + ), + pytest.param( + 4, + (0, 5), + ), + pytest.param( + 6, + (0, 4), + ), + pytest.param( + 10, + (0, 3), + ), + ], +) +def test_compile_and_run_constant_dot_correctness(size, input_range): + """Test correctness of results when running a compiled function""" + + low, high = input_range + shape = (size,) + + inputset = [ + (numpy.zeros(shape, dtype=numpy.uint32),), + (numpy.ones(shape, dtype=numpy.uint32) * high,), + ] + for _ in range(8): + inputset.append((numpy.random.randint(low, high + 1),)) + + constant = numpy.random.randint(low, high + 1, size=shape) + + def left(x): + return numpy.dot(x, constant) + + def right(x): + return numpy.dot(constant, x) + + left_circuit = compile_numpy_function( + left, + {"x": EncryptedTensor(Integer(64, False), shape)}, + inputset, + ) + right_circuit = compile_numpy_function( + left, + {"x": EncryptedTensor(Integer(64, False), shape)}, + inputset, + ) + + args = (numpy.random.randint(low, high + 1, size=shape).tolist(),) + assert left_circuit.run(*args) == left(*args) + assert right_circuit.run(*args) == right(*args) + + def test_compile_function_with_direct_tlu(): """Test compile_numpy_function_into_op_graph for a program with direct table lookup""" From ab1f0f3c4aa2f1eb19997d546f8f1688be78f0b8 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 15:25:11 +0200 Subject: [PATCH 0373/1104] chore: update env-docker tagging with compiler and concretefhe sha1 - also save the concretefhe sha1 in the container labels --- .github/workflows/continuous-integration.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 2241299cd..e4cdf748c 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -89,8 +89,10 @@ jobs: run: | PREFLIGHT_IMAGE_TAG=$(echo ${{ github.ref }} | sed -e 's/\//-/g') PREFLIGHT_IMAGE="${PREFLIGHT_IMAGE_BASE}-${PREFLIGHT_IMAGE_TAG}" + LABEL_SHA1=$(git rev-parse HEAD) echo "::set-output name=image::${PREFLIGHT_IMAGE}" echo "PREFLIGHT_IMAGE=${PREFLIGHT_IMAGE}" >> "$GITHUB_ENV" + echo "LABEL_SHA1=${LABEL_SHA1}" >> "$GITHUB_ENV" - name: Set up Docker Buildx if: ${{ fromJSON(env.BUILD_DOCKER) }} id: buildx @@ -109,9 +111,11 @@ jobs: context: . builder: ${{ steps.buildx.outputs.name }} file: docker/Dockerfile.concretefhe-env + no-cache: true push: true tags: "${{ env.PREFLIGHT_IMAGE }}" - no-cache: true + labels: | + concretefhe_sha=${{ env.LABEL_SHA1 }} - name: Set notification report id: report if: ${{ always() }} @@ -390,11 +394,12 @@ jobs: - name: Pull preflight image run: | docker pull "${PREFLIGHT_IMAGE}" - - name: Retag to latest and epoch-sha1 and push + - name: Retag to latest and zamalang_sha1-concretefhe_sha1 and push run: | - EPOCH=$(date +%s) SHA1=$(git rev-parse HEAD) - TAGGED_IMAGE="${BASE_IMAGE}:${EPOCH}-${SHA1}" + ZAMALANG_SHA1=$(docker inspect "${PREFLIGHT_IMAGE}" | \ + jq -rc '.[0].ContainerConfig.Labels["commit-sha"]') + TAGGED_IMAGE="${BASE_IMAGE}:${ZAMALANG_SHA1}-${SHA1}" docker tag "${PREFLIGHT_IMAGE}" "${LATEST_IMAGE}" docker tag "${PREFLIGHT_IMAGE}" "${TAGGED_IMAGE}" docker push "${LATEST_IMAGE}" From 2da3895f1aa4ea836613316a3fea68ee0989b732 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 6 Oct 2021 18:02:35 +0200 Subject: [PATCH 0374/1104] feat: add more bivariate operations which are supported if one of the operands is a constant refs #126 --- concrete/numpy/tracing.py | 67 +++++++++++-------- .../common/optimization/test_float_fusing.py | 34 +++++++++- tests/numpy/test_tracing.py | 5 +- 3 files changed, 74 insertions(+), 32 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 5cd78bc4f..47321b6b5 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -259,13 +259,13 @@ class NPTracer(BaseTracer): numpy.arctan, numpy.arctan2, numpy.arctanh, - # numpy.bitwise_and, - # numpy.bitwise_or, - # numpy.bitwise_xor, + numpy.bitwise_and, + numpy.bitwise_or, + numpy.bitwise_xor, numpy.cbrt, numpy.ceil, # numpy.conjugate, - # numpy.copysign, + numpy.copysign, numpy.cos, numpy.cosh, numpy.deg2rad, @@ -278,51 +278,51 @@ class NPTracer(BaseTracer): numpy.fabs, numpy.float_power, numpy.floor, - # numpy.floor_divide, - # numpy.fmax, - # numpy.fmin, - # numpy.fmod, + numpy.floor_divide, + numpy.fmax, + numpy.fmin, + numpy.fmod, # numpy.frexp, - # numpy.gcd, + numpy.gcd, # numpy.greater, # numpy.greater_equal, - # numpy.heaviside, - # numpy.hypot, + numpy.heaviside, + numpy.hypot, # numpy.invert, numpy.isfinite, numpy.isinf, numpy.isnan, # numpy.isnat, - # numpy.lcm, - # numpy.ldexp, - # numpy.left_shift, + numpy.lcm, + numpy.ldexp, + numpy.left_shift, # numpy.less, # numpy.less_equal, numpy.log, numpy.log10, numpy.log1p, numpy.log2, - # numpy.logaddexp, - # numpy.logaddexp2, - # numpy.logical_and, - # numpy.logical_not, - # numpy.logical_or, - # numpy.logical_xor, + numpy.logaddexp, + numpy.logaddexp2, + numpy.logical_and, + numpy.logical_not, + numpy.logical_or, + numpy.logical_xor, # numpy.matmul, - # numpy.maximum, - # numpy.minimum, + numpy.maximum, + numpy.minimum, # numpy.modf, # numpy.multiply, numpy.negative, - # numpy.nextafter, + numpy.nextafter, # numpy.not_equal, numpy.positive, - # numpy.power, + numpy.power, numpy.rad2deg, numpy.radians, numpy.reciprocal, - # numpy.remainder, - # numpy.right_shift, + numpy.remainder, + numpy.right_shift, numpy.rint, numpy.sign, numpy.signbit, @@ -334,7 +334,7 @@ class NPTracer(BaseTracer): # numpy.subtract, numpy.tan, numpy.tanh, - # numpy.true_divide, + numpy.true_divide, numpy.trunc, ] @@ -378,9 +378,18 @@ NPTracer.UFUNC_ROUTING = { fun: _get_unary_fun(fun) for fun in NPTracer.LIST_OF_SUPPORTED_UFUNC if fun.nin == 1 } -NPTracer.UFUNC_ROUTING[numpy.arctan2] = _get_binary_fun(numpy.arctan2) -NPTracer.UFUNC_ROUTING[numpy.float_power] = _get_binary_fun(numpy.float_power) +NPTracer.UFUNC_ROUTING.update( + {fun: _get_binary_fun(fun) for fun in NPTracer.LIST_OF_SUPPORTED_UFUNC if fun.nin == 2} +) +list_of_not_supported = [ + (ufunc.__name__, ufunc.nin) + for ufunc in NPTracer.LIST_OF_SUPPORTED_UFUNC + if ufunc.nin not in [1, 2] +] + +custom_assert(len(list_of_not_supported) == 0, f"Not supported nin's, {list_of_not_supported}") +del list_of_not_supported # We are adding initial support for `np.array(...)` +,-,* `BaseTracer` # (note that this is not the proper complete handling of these functions) diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 7f017ed7a..bbadfc9d3 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -189,16 +189,42 @@ def subtest_fuse_float_unary_operations_correctness(fun): assert function_to_trace(*inputs) == op_graph(*inputs) +LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES = { + numpy.bitwise_and, + numpy.bitwise_or, + numpy.bitwise_xor, + numpy.gcd, + numpy.lcm, + numpy.ldexp, + numpy.left_shift, + numpy.logical_and, + numpy.logical_not, + numpy.logical_or, + numpy.logical_xor, + numpy.remainder, + numpy.right_shift, +} + + def subtest_fuse_float_binary_operations_correctness(fun): """Test a binary functions with fuse_float_operations, with a constant as a source.""" for i in range(4): + # Know if the function is defined for integer inputs + if fun in LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES: + if i not in [0, 2]: + continue + + # The .astype(numpy.float64) that we have in cases 0 and 2 is here to force + # a float output even for functions which return an integer (eg, XOR), such + # that our frontend always try to fuse them + # For bivariate functions: fix one of the inputs if i == 0: # With an integer in first position def get_function_to_trace(): - return lambda x, y: fun(3, x + y).astype(numpy.int32) + return lambda x, y: fun(3, x + y).astype(numpy.float64).astype(numpy.int32) elif i == 1: # With a float in first position @@ -208,7 +234,7 @@ def subtest_fuse_float_binary_operations_correctness(fun): elif i == 2: # With an integer in second position def get_function_to_trace(): - return lambda x, y: fun(x + y, 4).astype(numpy.int32) + return lambda x, y: fun(x + y, 4).astype(numpy.float64).astype(numpy.int32) else: # With a float in second position @@ -217,6 +243,10 @@ def subtest_fuse_float_binary_operations_correctness(fun): input_list = [0, 2, 42, 44] + # Domain of definition + if fun in [numpy.true_divide, numpy.remainder, numpy.floor_divide, numpy.fmod]: + input_list = [2, 42, 44] + for input_ in input_list: function_to_trace = get_function_to_trace() diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index dffa405fc..7b8b9472a 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -406,8 +406,11 @@ def test_tracing_astype( ), ], ) +# numpy.logical_not is removed from the following test since it is expecting inputs which are +# integer only, as opposed to other functions in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC @pytest.mark.parametrize( - "function_to_trace_def", [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1] + "function_to_trace_def", + [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1 if f != numpy.logical_not], ) def test_trace_numpy_supported_unary_ufuncs(inputs, expected_output_node, function_to_trace_def): """Function to trace supported numpy ufuncs""" From e451afc283b51498bf8b1f1ec3048a85bfad67f1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 4 Oct 2021 14:01:47 +0200 Subject: [PATCH 0375/1104] chore: add semver and semantic-release dev dependencies - add __init__.py in tests to fix new pylint error --- poetry.lock | 411 +++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 7 + tests/__init__.py | 1 + 3 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py diff --git a/poetry.lock b/poetry.lock index 322f796c8..d101635f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -170,6 +170,17 @@ python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-log" +version = "0.3.2" +description = "Logging integration for Click" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" + [[package]] name = "colorama" version = "0.4.4" @@ -192,6 +203,25 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "35.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "cycler" version = "0.10.0" @@ -253,6 +283,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "dotty-dict" +version = "1.3.0" +description = "Dictionary wrapper for quick access to deeply nested keys." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +setuptools_scm = "*" + [[package]] name = "entrypoints" version = "0.3" @@ -289,6 +330,29 @@ flake8 = ">=3.0.0" [package.extras] dev = ["coverage", "black", "hypothesis", "hypothesmith"] +[[package]] +name = "gitdb" +version = "4.0.7" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<5" + +[[package]] +name = "gitpython" +version = "3.1.24" +description = "GitPython is a python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} + [[package]] name = "idna" version = "3.2" @@ -305,6 +369,22 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "importlib-metadata" +version = "4.8.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + [[package]] name = "inflect" version = "5.3.0" @@ -325,6 +405,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "invoke" +version = "1.6.0" +description = "Pythonic task execution" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "ipykernel" version = "6.4.1" @@ -435,6 +523,18 @@ parso = ">=0.8.0,<0.9.0" qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] +[[package]] +name = "jeepney" +version = "0.7.1" +description = "Low-level, pure Python DBus protocol wrapper." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] +trio = ["trio", "async-generator"] + [[package]] name = "jinja2" version = "3.0.2" @@ -563,6 +663,24 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "keyring" +version = "23.2.1" +description = "Store and access your passwords safely." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = ">=3.6" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + [[package]] name = "kiwisolver" version = "1.3.2" @@ -952,6 +1070,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "pkginfo" +version = "1.7.1" +description = "Query metadatdata from sdists / bdists / installed packages." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +testing = ["nose", "coverage"] + [[package]] name = "platformdirs" version = "2.4.0" @@ -1193,6 +1322,48 @@ python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-gitlab" +version = "2.10.1" +description = "Interact with GitLab API" +category = "dev" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +requests = ">=2.25.0" +requests-toolbelt = ">=0.9.1" + +[package.extras] +autocompletion = ["argcomplete (>=1.10.0,<2)"] +yaml = ["PyYaml (>=5.2)"] + +[[package]] +name = "python-semantic-release" +version = "7.19.2" +description = "Automatic Semantic Versioning for Python projects" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=7,<9" +click-log = ">=0.3,<1" +dotty-dict = ">=1.3.0,<2" +gitpython = ">=3.0.8,<4" +invoke = ">=1.4.1,<2" +python-gitlab = ">=1.10,<3" +requests = ">=2.25,<3" +semver = ">=2.10,<3" +tomlkit = "0.7.0" +twine = ">=3,<4" + +[package.extras] +dev = ["tox", "isort", "black"] +docs = ["Sphinx (==1.3.6)"] +mypy = ["mypy", "types-requests"] +test = ["coverage (>=5,<6)", "pytest (>=5,<6)", "pytest-xdist (>=1,<2)", "pytest-mock (>=2,<3)", "responses (==0.13.3)", "mock (==1.3.0)"] + [[package]] name = "pytz" version = "2021.3" @@ -1209,6 +1380,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pywinpty" version = "1.1.4" @@ -1267,6 +1446,22 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +[[package]] +name = "readme-renderer" +version = "30.0" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bleach = ">=2.1.0" +docutils = ">=0.13.1" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.5.0,<0.7.0)"] + [[package]] name = "regex" version = "2021.9.30" @@ -1293,6 +1488,48 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "secretstorage" +version = "3.3.1" +description = "Python bindings to FreeDesktop.org Secret Service API" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "send2trash" version = "1.8.0" @@ -1306,6 +1543,21 @@ nativelib = ["pyobjc-framework-cocoa", "pywin32"] objc = ["pyobjc-framework-cocoa"] win32 = ["pywin32"] +[[package]] +name = "setuptools-scm" +version = "6.3.2" +description = "the blessed package to manage your versions by scm tags" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = ">=20.0" +tomli = ">=1.0.0" + +[package.extras] +toml = ["setuptools (>=42)", "tomli (>=1.0.0)"] + [[package]] name = "six" version = "1.16.0" @@ -1314,6 +1566,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "smmap" +version = "4.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "snowballstemmer" version = "2.1.0" @@ -1497,6 +1757,14 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "tomlkit" +version = "0.7.0" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "tornado" version = "6.1" @@ -1532,6 +1800,25 @@ python-versions = ">=3.7" [package.extras] test = ["pytest"] +[[package]] +name = "twine" +version = "3.4.2" +description = "Collection of utilities for publishing packages on PyPI" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = ">=0.4.3" +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.4.2" +readme-renderer = ">=21.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +tqdm = ">=4.14" + [[package]] name = "typing-extensions" version = "3.10.0.2" @@ -1599,10 +1886,22 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "f0205455ef0cf713936e92ef934fc6f850ca9aa46cf59c4151cc9deff4f37958" +content-hash = "1c69a2c569fdacbcf43e47d88dc2d93ff098c50395f94c93b05428202f047cb6" [metadata.files] alabaster = [ @@ -1717,6 +2016,10 @@ click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, ] +click-log = [ + {file = "click-log-0.3.2.tar.gz", hash = "sha256:16fd1ca3fc6b16c98cea63acf1ab474ea8e676849dc669d86afafb0ed7003124"}, + {file = "click_log-0.3.2-py2.py3-none-any.whl", hash = "sha256:eee14dc37cdf3072158570f00406572f9e03e414accdccfccd4c538df9ae322c"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, @@ -1758,6 +2061,28 @@ coverage = [ {file = "coverage-6.0-pp37-none-any.whl", hash = "sha256:fff04bfefb879edcf616f1ce5ea6f4a693b5976bdc5e163f8464f349c25b59f0"}, {file = "coverage-6.0.tar.gz", hash = "sha256:17983f6ccc47f4864fd16d20ff677782b23d1207bf222d10e4d676e4636b0872"}, ] +cryptography = [ + {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, + {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"}, + {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"}, + {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"}, + {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"}, +] cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, @@ -1801,6 +2126,9 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] +dotty-dict = [ + {file = "dotty_dict-1.3.0.tar.gz", hash = "sha256:eb0035a3629ecd84397a68f1f42f1e94abd1c34577a19cd3eacad331ee7cbaf0"}, +] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, @@ -1813,6 +2141,14 @@ flake8-bugbear = [ {file = "flake8-bugbear-21.9.2.tar.gz", hash = "sha256:db9a09893a6c649a197f5350755100bb1dd84f110e60cf532fdfa07e41808ab2"}, {file = "flake8_bugbear-21.9.2-py36.py37.py38-none-any.whl", hash = "sha256:4f7eaa6f05b7d7ea4cbbde93f7bcdc5438e79320fa1ec420d860c181af38b769"}, ] +gitdb = [ + {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, + {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, +] +gitpython = [ + {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, + {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, +] idna = [ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, @@ -1821,6 +2157,10 @@ imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] +importlib-metadata = [ + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, +] inflect = [ {file = "inflect-5.3.0-py3-none-any.whl", hash = "sha256:42560be16af702a21d43d59427f276b5aed79efb1ded9b713468c081f4353d10"}, {file = "inflect-5.3.0.tar.gz", hash = "sha256:41a23f6788962e9775e40e2ecfb1d6455d02de315022afeedd3c5dc070019d73"}, @@ -1829,6 +2169,11 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +invoke = [ + {file = "invoke-1.6.0-py2-none-any.whl", hash = "sha256:e6c9917a1e3e73e7ea91fdf82d5f151ccfe85bf30cc65cdb892444c02dbb5f74"}, + {file = "invoke-1.6.0-py3-none-any.whl", hash = "sha256:769e90caeb1bd07d484821732f931f1ad8916a38e3f3e618644687fc09cb6317"}, + {file = "invoke-1.6.0.tar.gz", hash = "sha256:374d1e2ecf78981da94bfaf95366216aaec27c2d6a7b7d5818d92da55aa258d3"}, +] ipykernel = [ {file = "ipykernel-6.4.1-py3-none-any.whl", hash = "sha256:a3f6c2dda2ecf63b37446808a70ed825fea04790779ca524889c596deae0def8"}, {file = "ipykernel-6.4.1.tar.gz", hash = "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd"}, @@ -1853,6 +2198,10 @@ jedi = [ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] +jeepney = [ + {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, + {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, +] jinja2 = [ {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, @@ -1890,6 +2239,10 @@ jupyterlab-widgets = [ {file = "jupyterlab_widgets-1.0.2-py3-none-any.whl", hash = "sha256:f5d9efface8ec62941173ba1cffb2edd0ecddc801c11ae2931e30b50492eb8f7"}, {file = "jupyterlab_widgets-1.0.2.tar.gz", hash = "sha256:7885092b2b96bf189c3a705cc3c412a4472ec5e8382d0b47219a66cccae73cfa"}, ] +keyring = [ + {file = "keyring-23.2.1-py3-none-any.whl", hash = "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"}, + {file = "keyring-23.2.1.tar.gz", hash = "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe"}, +] kiwisolver = [ {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6"}, {file = "kiwisolver-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470"}, @@ -2239,6 +2592,10 @@ pillow = [ {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] +pkginfo = [ + {file = "pkginfo-1.7.1-py2.py3-none-any.whl", hash = "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779"}, + {file = "pkginfo-1.7.1.tar.gz", hash = "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"}, +] platformdirs = [ {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, @@ -2390,6 +2747,14 @@ python-dotenv = [ {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, ] +python-gitlab = [ + {file = "python-gitlab-2.10.1.tar.gz", hash = "sha256:7afa7d7c062fa62c173190452265a30feefb844428efc58ea5244f3b9fc0d40f"}, + {file = "python_gitlab-2.10.1-py3-none-any.whl", hash = "sha256:581a219759515513ea9399e936ed7137437cfb681f52d2641626685c492c999d"}, +] +python-semantic-release = [ + {file = "python-semantic-release-7.19.2.tar.gz", hash = "sha256:8ca0e5f72d31e7b0603b95caad6fb2d5315483ac1fadd86648771966d9ec6f2c"}, + {file = "python_semantic_release-7.19.2-py3-none-any.whl", hash = "sha256:b2c8bb16a643fee0831be4d06138bc1440ebd4f252c3397d41abde179ea56852"}, +] pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, @@ -2406,6 +2771,10 @@ pywin32 = [ {file = "pywin32-301-cp39-cp39-win32.whl", hash = "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe"}, {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"}, ] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] pywinpty = [ {file = "pywinpty-1.1.4-cp36-none-win_amd64.whl", hash = "sha256:fb975976ad92be44801de95fdf2b0366747767cb0528478553aff85dd63ebb09"}, {file = "pywinpty-1.1.4-cp37-none-win_amd64.whl", hash = "sha256:5d25b30a2f87105778bc2f57cb1271f58aaa25568921ef042faf001b3b0a7307"}, @@ -2491,6 +2860,10 @@ qtpy = [ {file = "QtPy-1.11.2-py2.py3-none-any.whl", hash = "sha256:83c502973e9fdd7b648d8267a421229ea3d9a0651c22e4c65a4d9228479c39b6"}, {file = "QtPy-1.11.2.tar.gz", hash = "sha256:d6e4ae3a41f1fcb19762b58f35ad6dd443b4bdc867a4cb81ef10ccd85403c92b"}, ] +readme-renderer = [ + {file = "readme_renderer-30.0-py2.py3-none-any.whl", hash = "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc"}, + {file = "readme_renderer-30.0.tar.gz", hash = "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"}, +] regex = [ {file = "regex-2021.9.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66696c8336a1b5d1182464f3af3427cc760118f26d0b09a2ddc16a976a4d2637"}, {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d87459ad3ab40cd8493774f8a454b2e490d8e729e7e402a0625867a983e4e02"}, @@ -2538,14 +2911,38 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +secretstorage = [ + {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, + {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, +] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] send2trash = [ {file = "Send2Trash-1.8.0-py3-none-any.whl", hash = "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08"}, {file = "Send2Trash-1.8.0.tar.gz", hash = "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d"}, ] +setuptools-scm = [ + {file = "setuptools_scm-6.3.2-py3-none-any.whl", hash = "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119"}, + {file = "setuptools_scm-6.3.2.tar.gz", hash = "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +smmap = [ + {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, + {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, +] snowballstemmer = [ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, @@ -2602,6 +2999,10 @@ tomli = [ {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, ] +tomlkit = [ + {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"}, + {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"}, +] tornado = [ {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, @@ -2653,6 +3054,10 @@ traitlets = [ {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, ] +twine = [ + {file = "twine-3.4.2-py3-none-any.whl", hash = "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218"}, + {file = "twine-3.4.2.tar.gz", hash = "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"}, +] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, @@ -2681,3 +3086,7 @@ win32-setctime = [ wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/pyproject.toml b/pyproject.toml index c00b03639..6249b69fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ py-cpuinfo = "^8.0.0" python-dotenv = "^0.19.0" sphinx-copybutton = "^0.4.0" nbmake = "^0.9" +python-semantic-release = "^7.19.2" +semver = "^2.13.0" [build-system] requires = ["poetry-core>=1.0.0"] @@ -47,3 +49,8 @@ build-backend = "poetry.core.masonry.api" filterwarnings = [ "error" ] + +[tool.semantic_release] +version_toml = "pyproject.toml:tool.poetry.version" +version_variable = "concrete/version.py:__version__,docs/conf.py:release" +upload_to_pypi = "False" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..1e38b00cd --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test module.""" From b363db670020dc109357c8ca8c8d6a478268a153 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 4 Oct 2021 14:41:37 +0200 Subject: [PATCH 0376/1104] chore(tools): centralize all versions related utils in a single script - update version to be semver compliant with the new tools - update make targets and CI workflow to use the new version tool - update release issue template --- .github/ISSUE_TEMPLATE/release.md | 18 +- .github/workflows/continuous-integration.yaml | 3 +- Makefile | 12 +- concrete/version.py | 2 +- docs/conf.py | 2 +- poetry.lock | 2 +- pyproject.toml | 3 +- script/actions_utils/version_comparison.py | 51 ---- script/make_utils/set_version.sh | 60 ---- script/make_utils/version_utils.py | 266 ++++++++++++++++++ 10 files changed, 284 insertions(+), 135 deletions(-) delete mode 100644 script/actions_utils/version_comparison.py delete mode 100755 script/make_utils/set_version.sh create mode 100644 script/make_utils/version_utils.py diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 104c67119..ba66bc187 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -1,7 +1,7 @@ --- name: Release about: Issue template to prepare a release step by step. -title: "Release vX.Y.Z (or vX.Y.Zrc?)" +title: "Release vX.Y.Z (or vX.Y.Z-rc?)" --- Please check all steps if it was either done/already done, at the end of a release all check boxes must have been checked. @@ -9,20 +9,20 @@ Please check all steps if it was either done/already done, at the end of a relea Release check-list: If it was not already done: -- [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Zrc?` for Release Candidates) following semantic versioning: https://semver.org/ -- [ ] Update the project version to `X.Y.Z` (or `X.Y.Zrc?`) by running: +- [ ] Choose the version number, e.g. `vX.Y.Z` (can be `vX.Y.Z-rc?` for Release Candidates) following semantic versioning: https://semver.org/ +- [ ] Update the project version to `X.Y.Z` (or `X.Y.Z-rc?`) by running: ```bash VERSION=X.Y.Z make set_version # or -VERSION=X.Y.Zrc? make set_version +VERSION=X.Y.Z-rc? make set_version ``` Then: - [ ] For non RC releases: check the release milestone issues, cut out what can't be completed in time and change the milestones for these issues -- [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Zrc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Zrc?`) +- [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Z-rc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Z-rc?`) - [ ] Wait for the release workflow to finish and get the image url from the notification or the logs -- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link copied from the step before \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Zrc?`) for the uploaded docker image. Mark release as pre-release for an `rc` version. See template below: +- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link copied from the step before \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Z-rc?`) for the uploaded docker image. Mark release as pre-release for an `rc` version. See template below: This is the release markdown template you should copy and update: ``` @@ -31,13 +31,13 @@ This is the release markdown template you should copy and update: ``` To continue the release cycle: -- [ ] Choose the version number for next release, e.g. `vA.B.C` (can be `vA.B.Crc?` for Release Candidates) following semantic versioning: https://semver.org/ -- [ ] Update the project version to `A.B.C` (or `A.B.Crc?`) by running: +- [ ] Choose the version number for next release, e.g. `vA.B.C` (can be `vA.B.C-rc?` for Release Candidates) following semantic versioning: https://semver.org/ +- [ ] Update the project version to `A.B.C` (or `A.B.C-rc?`) by running: ```bash VERSION=A.B.C make set_version # or -VERSION=A.B.Crc? make set_version +VERSION=A.B.C-rc? make set_version ``` All done! diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index e4cdf748c..45969264e 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -460,7 +460,8 @@ jobs: # We want the space separated list of versions to be expanded # shellcheck disable=SC2086 - REQUIRES_LATEST_TAG=$(python script/actions_utils/version_comparison.py \ + REQUIRES_LATEST_TAG=$(python script/make_utils/version_utils.py \ + islatest \ --new-version "${GIT_TAG}" \ --existing-versions $EXISTING_TAGS) diff --git a/Makefile b/Makefile index feb2c0f4a..fa5b4b85c 100644 --- a/Makefile +++ b/Makefile @@ -222,17 +222,9 @@ set_version: echo "VERSION env variable is empty. Please set to desired version."; \ exit 1; \ fi; - ./script/make_utils/set_version.sh --version "$${VERSION}" --src-dir "$(SRC_DIR)" + poetry run python ./script/make_utils/version_utils.py set-version --version "$${VERSION}" .PHONY: set_version check_version_coherence: - @SRC_VER=$$(poetry run python -c "from $(SRC_DIR) import __version__; print(__version__);");\ - PROJECT_VER=($$(poetry version)); \ - PROJECT_VER="$${PROJECT_VER[1]}"; \ - echo "Source version: $${SRC_VER}"; \ - echo "Project version: $${PROJECT_VER}"; \ - if [[ "$${SRC_VER}" != "$${PROJECT_VER}" ]]; then \ - echo "Version mismatch between source and pyproject.toml re-run make set_version"; \ - exit 1; \ - fi + poetry run python ./script/make_utils/version_utils.py check-version .PHONY: check_version_coherence diff --git a/concrete/version.py b/concrete/version.py index c194a9e64..b5df944c9 100644 --- a/concrete/version.py +++ b/concrete/version.py @@ -1,4 +1,4 @@ """Package version module.""" # Auto-generated by "make set_version" do not modify -__version__ = "0.2.0rc1" +__version__ = "0.2.0-rc1" diff --git a/docs/conf.py b/docs/conf.py index 7fb681920..158558e83 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ copyright = "2021, Zama" author = "Zama" # The full version, including alpha/beta/rc tags -release = "0.1" +release = "0.2.0-rc1" # -- General configuration --------------------------------------------------- diff --git a/poetry.lock b/poetry.lock index d101635f4..8af1c6336 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1901,7 +1901,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "1c69a2c569fdacbcf43e47d88dc2d93ff098c50395f94c93b05428202f047cb6" +content-hash = "783e41a9b79babbea7b986de2d4b901e2472e202613952e9881ecf340ff69d18" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 6249b69fd..3d721fa38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.2.0rc1" +version = "0.2.0-rc1" description = "Concrete Framework" authors = ["Zama "] packages = [ @@ -40,6 +40,7 @@ sphinx-copybutton = "^0.4.0" nbmake = "^0.9" python-semantic-release = "^7.19.2" semver = "^2.13.0" +tomlkit = "^0.7.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/script/actions_utils/version_comparison.py b/script/actions_utils/version_comparison.py deleted file mode 100644 index f78fd9b35..000000000 --- a/script/actions_utils/version_comparison.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Helper script for github actions to compare versions""" -import argparse -import re -import sys - - -def main(args): - """Entry point""" - print(args, file=sys.stderr) - semver_matcher = re.compile(r"^(v)?([\d.]+)(rc\d+)?$") - # Keep versions that are not release candidate - all_versions = [ - tuple(map(int, match.group(2).split("."))) - for version in args.existing_versions - if (match := semver_matcher.match(version)) is not None and match.group(3) is None - ] - - nv_match = semver_matcher.match(args.new_version) - new_version = ( - tuple(map(int, nv_match.group(2).split("."))) - if nv_match is not None and nv_match.group(3) is None - else None - ) - - all_versions.append(new_version) - - nv_is_rc = new_version is None - nv_is_latest = not nv_is_rc and max(all_versions) == new_version - print(str(nv_is_latest).lower()) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - "Compare new version to previous versions and determine if it's the latest", - allow_abbrev=False, - ) - - parser.add_argument("--new-version", type=str, required=True, help="The new version to compare") - parser.add_argument( - "--existing-versions", - type=str, - nargs="+", - required=True, - help="The list of existing versions", - ) - - cli_args = parser.parse_args() - - main(cli_args) diff --git a/script/make_utils/set_version.sh b/script/make_utils/set_version.sh deleted file mode 100755 index 0c75efa91..000000000 --- a/script/make_utils/set_version.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash - -VERSION_TO_SET= -SRC_DIR= - -while [ -n "$1" ] -do - case "$1" in - "--version" ) - shift - VERSION_TO_SET="$1" - ;; - - "--src-dir" ) - shift - SRC_DIR="$1" - ;; - - *) - echo "Unknown param : $1" - exit 1 - ;; - esac - shift -done - -if [[ "${VERSION_TO_SET}" == "" ]]; then - echo "--version is required. Aborting" - exit 1 -fi - -if [[ "${SRC_DIR}" == "" ]]; then - echo "--src-dir is required. Aborting" - exit 1 -fi - -rx='^(v)?([0-9]+\.){2}[0-9]+(rc[0-9]+)?$' - -if [[ ! "${VERSION_TO_SET}" =~ $rx ]]; then - echo "ERROR: Unable to validate version: '${VERSION_TO_SET}'" - exit 1 -fi - -echo "INFO: Version ${VERSION_TO_SET}" - -VERSION_TO_SET="${VERSION_TO_SET/v/}" -echo "${VERSION_TO_SET}" - -poetry version "${VERSION_TO_SET}" - -VERSION_FILE="${SRC_DIR}/version.py" - -rm "${VERSION_FILE}" - -{ - echo '"""Package version module."""' - echo '# Auto-generated by "make set_version" do not modify' - echo '' - echo "__version__ = \"${VERSION_TO_SET}\"" -} >> "${VERSION_FILE}" diff --git a/script/make_utils/version_utils.py b/script/make_utils/version_utils.py new file mode 100644 index 000000000..05e0ff3d2 --- /dev/null +++ b/script/make_utils/version_utils.py @@ -0,0 +1,266 @@ +"""Tool to manage version in the project""" + +import argparse +import os +import re +import sys +from pathlib import Path +from typing import List, Optional + +import tomlkit +from semver import VersionInfo + + +def strip_leading_v(version_str: str): + """Strip leading v of a version which is not SemVer compatible.""" + if version_str and version_str[0] == "v": + return version_str[1:] + return version_str + + +def islatest(args): + """islatest command entry point.""" + print(args, file=sys.stderr) + + new_version_is_latest = False + + new_version_str = strip_leading_v(args.new_version) + if VersionInfo.isvalid(new_version_str): + new_version_info = VersionInfo.parse(new_version_str) + if new_version_info.prerelease is None: + # If it's an actual release + all_versions_str = ( + strip_leading_v(version_str) for version_str in args.existing_versions + ) + + # Keep versions that are not release candidate + all_non_prerelease_version_infos = [ + version_info + for version_str in all_versions_str + if VersionInfo.isvalid(version_str) + and (version_info := VersionInfo.parse(version_str)) + and version_info.prerelease is None + ] + + all_non_prerelease_version_infos.append(new_version_info) + + new_version_is_latest = max(all_non_prerelease_version_infos) == new_version_info + print(str(new_version_is_latest).lower()) + + +def update_variable_in_py_file(file_path: Path, var_name: str, version_str: str): + """Update the version in a .py file.""" + + file_content = None + with open(file_path, encoding="utf-8") as f: + file_content = f.read() + + updated_file_content = re.sub( + rf'{var_name} *[:=] *["\'](.+)["\']', + rf'{var_name} = "{version_str}"', + file_content, + ) + + with open(file_path, "w", encoding="utf-8", newline="\n") as f: + f.write(updated_file_content) + + +def update_variable_in_toml_file(file_path: Path, var_name: str, version_str: str): + """Update the version in a .toml file.""" + toml_content = None + with open(file_path, encoding="utf-8") as f: + toml_content = tomlkit.loads(f.read()) + + toml_keys = var_name.split(".") + current_content = toml_content + for toml_key in toml_keys[:-1]: + current_content = current_content[toml_key] + last_toml_key = toml_keys[-1] + current_content[last_toml_key] = version_str + + with open(file_path, "w", encoding="utf-8", newline="\n") as f: + f.write(tomlkit.dumps(toml_content)) + + +def load_file_vars_set(pyproject_path: os.PathLike, cli_file_vars: Optional[List[str]]): + """Load files and their version variables set-up in pyproject.toml and passed as arguments.""" + + file_vars_set = set() + if cli_file_vars is not None: + file_vars_set.update(cli_file_vars) + + pyproject_path = Path(pyproject_path).resolve() + + # Check if there is a semantic release configuration + if pyproject_path.exists(): + pyproject_content = None + with open(pyproject_path, encoding="utf-8") as f: + pyproject_content = tomlkit.loads(f.read()) + + try: + sr_conf = pyproject_content["tool"]["semantic_release"] + sr_version_toml: str = sr_conf.get("version_toml", "") + file_vars_set.update(sr_version_toml.split(",")) + sr_version_variable: str = sr_conf.get("version_variable", "") + file_vars_set.update(sr_version_variable.split(",")) + except KeyError: + print("No configuration for semantic release in pyproject.toml") + + return file_vars_set + + +def set_version(args): + """set-version command entry point.""" + + version_str = strip_leading_v(args.version) + if not VersionInfo.isvalid(version_str): + raise RuntimeError(f"Unable to validate version: {args.version}") + + file_vars_set = load_file_vars_set(args.pyproject_file, args.file_vars) + + for file_var_str in sorted(file_vars_set): + print(f"Processing {file_var_str}") + file, var_name = file_var_str.split(":", 1) + file_path = Path(file).resolve() + + if file_path.suffix == ".py": + update_variable_in_py_file(file_path, var_name, version_str) + elif file_path.suffix == ".toml": + update_variable_in_toml_file(file_path, var_name, version_str) + else: + raise RuntimeError(f"Unsupported file extension: {file_path.suffix}") + + +def get_variable_from_py_file(file_path: Path, var_name: str): + """Read variable value from a .py file.""" + file_content = None + with open(file_path, encoding="utf-8") as f: + file_content = f.read() + + variable_values_set = set() + + start_pos = 0 + while True: + file_content = file_content[start_pos:] + match = re.search( + rf'{var_name} *[:=] *["\'](.+)["\']', + file_content, + ) + if match is None: + break + + variable_values_set.add(match.group(1)) + start_pos = match.end() + + return variable_values_set + + +def get_variable_from_toml_file(file_path: Path, var_name: str): + """Read variable value from a .toml file.""" + + toml_content = None + with open(file_path, encoding="utf-8") as f: + toml_content = tomlkit.loads(f.read()) + + toml_keys = var_name.split(".") + current_content = toml_content + for toml_key in toml_keys: + current_content = current_content[toml_key] + + return current_content + + +def check_version(args): + """check-version command entry point.""" + + version_str_set = set() + + file_vars_set = load_file_vars_set(args.pyproject_file, args.file_vars) + + for file_var_str in sorted(file_vars_set): + print(f"Processing {file_var_str}") + file, var_name = file_var_str.split(":", 1) + file_path = Path(file).resolve() + + if file_path.suffix == ".py": + version_str_set.update(get_variable_from_py_file(file_path, var_name)) + elif file_path.suffix == ".toml": + version_str_set.add(get_variable_from_toml_file(file_path, var_name)) + else: + raise RuntimeError(f"Unsupported file extension: {file_path.suffix}") + + if len(version_str_set) == 0: + raise RuntimeError(f"No versions found in {', '.join(sorted(file_vars_set))}") + if len(version_str_set) > 1: + raise RuntimeError( + f"Found more than one version: {', '.join(sorted(version_str_set))}\n" + "Re-run make set-version" + ) + # Now version_str_set len == 1 + if not VersionInfo.isvalid((version := next(iter(version_str_set)))): + raise RuntimeError(f"Unable to validate version: {version}") + + print(f"Found version {version} in all processed locations.") + + +def main(args): + """Entry point""" + args.entry_point(args) + + +if __name__ == "__main__": + main_parser = argparse.ArgumentParser("Version utils", allow_abbrev=False) + + sub_parsers = main_parser.add_subparsers(dest="sub-command", required=True) + + parser_islatest = sub_parsers.add_parser("islatest") + parser_islatest.add_argument( + "--new-version", type=str, required=True, help="The new version to compare" + ) + parser_islatest.add_argument( + "--existing-versions", + type=str, + nargs="+", + required=True, + help="The list of existing versions", + ) + parser_islatest.set_defaults(entry_point=islatest) + + parser_set_version = sub_parsers.add_parser("set-version") + parser_set_version.add_argument("--version", type=str, required=True, help="The version to set") + parser_set_version.add_argument( + "--pyproject-file", + type=str, + default="pyproject.toml", + help="The path to a project's pyproject.toml file, defaults to $pwd/pyproject.toml", + ) + parser_set_version.add_argument( + "--file-vars", + type=str, + nargs="+", + help=( + "A space separated list of file/path.{py, toml}:variable to update with the new version" + ), + ) + parser_set_version.set_defaults(entry_point=set_version) + + parser_check_version = sub_parsers.add_parser("check-version") + parser_check_version.add_argument( + "--pyproject-file", + type=str, + default="pyproject.toml", + help="The path to a project's pyproject.toml file, defaults to $pwd/pyproject.toml", + ) + parser_check_version.add_argument( + "--file-vars", + type=str, + nargs="+", + help=( + "A space separated list of file/path.{py, toml}:variable to update with the new version" + ), + ) + parser_check_version.set_defaults(entry_point=check_version) + + cli_args = main_parser.parse_args() + + main(cli_args) From a595139448570c6984282e0c1e09cf473c822e6e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 6 Oct 2021 09:16:37 +0200 Subject: [PATCH 0377/1104] chore: add changelog_helper to generate changelogs using semantic-release - freeze semantic-release to specific version to keep internal APIs stable - add gitpyhton for changelog_helper - add a target to very easily create a changelog --- Makefile | 6 + poetry.lock | 8 +- pyproject.toml | 3 +- script/make_utils/changelog_helper.py | 248 ++++++++++++++++++++++++++ script/make_utils/version_utils.py | 4 +- 5 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 script/make_utils/changelog_helper.py diff --git a/Makefile b/Makefile index fa5b4b85c..12b7b600b 100644 --- a/Makefile +++ b/Makefile @@ -228,3 +228,9 @@ set_version: check_version_coherence: poetry run python ./script/make_utils/version_utils.py check-version .PHONY: check_version_coherence + +changelog: check_version_coherence + PROJECT_VER=($$(poetry version));\ + PROJECT_VER="$${PROJECT_VER[1]}";\ + poetry run python ./script/make_utils/changelog_helper.py > "CHANGELOG_$${PROJECT_VER}.md" +.PHONY: changelog diff --git a/poetry.lock b/poetry.lock index 8af1c6336..a7a3574a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -595,7 +595,7 @@ qtconsole = "*" [[package]] name = "jupyter-client" -version = "7.0.5" +version = "7.0.6" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false @@ -1901,7 +1901,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "783e41a9b79babbea7b986de2d4b901e2472e202613952e9881ecf340ff69d18" +content-hash = "b044859111d093c4bc499eed965c77062ebddab73f46f8b704edc801868696f5" [metadata.files] alabaster = [ @@ -2220,8 +2220,8 @@ jupyter = [ {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, ] jupyter-client = [ - {file = "jupyter_client-7.0.5-py3-none-any.whl", hash = "sha256:124a6e6979c38999d9153b1c4d1808c4c820a45066d5ed1857a5b59c04ffccb3"}, - {file = "jupyter_client-7.0.5.tar.gz", hash = "sha256:382aca66dcaf96d7eaaa6c546d57cdf8b3b1cf5bc1f2704c58a1d8d244f1163d"}, + {file = "jupyter_client-7.0.6-py3-none-any.whl", hash = "sha256:074bdeb1ffaef4a3095468ee16313938cfdc48fc65ca95cc18980b956c2e5d79"}, + {file = "jupyter_client-7.0.6.tar.gz", hash = "sha256:8b6e06000eb9399775e0a55c52df6c1be4766666209c22f90c2691ded0e338dc"}, ] jupyter-console = [ {file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"}, diff --git a/pyproject.toml b/pyproject.toml index 3d721fa38..d880b1809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,10 @@ py-cpuinfo = "^8.0.0" python-dotenv = "^0.19.0" sphinx-copybutton = "^0.4.0" nbmake = "^0.9" -python-semantic-release = "^7.19.2" +python-semantic-release = "7.19.2" semver = "^2.13.0" tomlkit = "^0.7.0" +GitPython = "^3.1.24" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/script/make_utils/changelog_helper.py b/script/make_utils/changelog_helper.py new file mode 100644 index 000000000..8adfa616b --- /dev/null +++ b/script/make_utils/changelog_helper.py @@ -0,0 +1,248 @@ +"""Tool to bypass the insane logic of semantic-release and generate changelogs we want""" + +import argparse +import subprocess +import sys +from collections import deque + +from git.repo import Repo +from semantic_release.changelog import markdown_changelog +from semantic_release.errors import UnknownCommitMessageStyleError +from semantic_release.settings import config, current_commit_parser +from semantic_release.vcs_helpers import get_repository_owner_and_name +from semver import VersionInfo + + +def log_msg(*args, file=sys.stderr, **kwargs): + """Shortcut to print to sys.stderr.""" + print(*args, file=file, **kwargs) + + +def strip_leading_v(version_str: str): + """Strip leading v of a version which is not SemVer compatible.""" + return version_str[1:] if version_str.startswith("v") else version_str + + +def get_poetry_project_version() -> VersionInfo: + """Run poetry version and get the project version""" + command = ["poetry", "version"] + poetry_version_output = subprocess.check_output(command, text=True) + return version_string_to_version_info(poetry_version_output.split(" ")[1]) + + +def raise_exception_or_print_warning(is_error: bool, message_body: str): + """Raise an exception if is_error is true else print a warning to stderr""" + msg_start = "Error" if is_error else "Warning" + msg = f"{msg_start}: {message_body}" + if is_error: + raise RuntimeError(msg) + log_msg(msg) + + +def version_string_to_version_info(version_string: str) -> VersionInfo: + """Convert git tag to VersionInfo.""" + return VersionInfo.parse(strip_leading_v(version_string)) + + +def generate_changelog(repo: Repo, from_commit_excluded: str, to_commit_included: str) -> dict: + """Recreate the functionality from semantic release with the from and to commits. + + Args: + repo (Repo): the gitpython Repo object representing your git repository + from_commit_excluded (str): the commit after which we want to collect commit messages for + the changelog + to_commit_included (str): the last commit included in the collected commit messages for the + changelog. + + Returns: + dict: the same formatted dict as the generate_changelog from semantic-release + """ + # Additional sections will be added as new types are encountered + changes: dict = {"breaking": []} + + rev = f"{from_commit_excluded}...{to_commit_included}" + + for commit in repo.iter_commits(rev): + hash_ = commit.hexsha + commit_message = ( + commit.message.replace("\r\n", "\n") + if isinstance(commit.message, str) + else commit.message.replace(b"\r\n", b"\n") + ) + try: + message = current_commit_parser()(commit_message) + if message.type not in changes: + log_msg(f"Creating new changelog section for {message.type} ") + changes[message.type] = [] + + # Capitalize the first letter of the message, leaving others as they were + # (using str.capitalize() would make the other letters lowercase) + formatted_message = message.descriptions[0][0].upper() + message.descriptions[0][1:] + if config.get("changelog_capitalize") is False: + formatted_message = message.descriptions[0] + + # By default, feat(x): description shows up in changelog with the + # scope bolded, like: + # + # * **x**: description + if config.get("changelog_scope") and message.scope: + formatted_message = f"**{message.scope}:** {formatted_message}" + + changes[message.type].append((hash_, formatted_message)) + + if message.breaking_descriptions: + # Copy breaking change descriptions into changelog + for paragraph in message.breaking_descriptions: + changes["breaking"].append((hash_, paragraph)) + elif message.bump == 3: + # Major, but no breaking descriptions, use commit subject instead + changes["breaking"].append((hash_, message.descriptions[0])) + + except UnknownCommitMessageStyleError as err: + log_msg(f"Ignoring UnknownCommitMessageStyleError: {err}") + + return changes + + +def main(args): + """Entry point""" + + repo = Repo(args.repo_root) + + sha1_to_tags = {tag.commit.hexsha: tag for tag in repo.tags} + + to_commit = repo.commit(args.to_ref) + log_msg(f"To commit: {to_commit}") + + to_tag = sha1_to_tags.get(to_commit.hexsha, None) + if to_tag is None: + raise_exception_or_print_warning( + is_error=args.to_ref_must_have_tag, + message_body=f"to-ref {args.to_ref} has no tag associated to it", + ) + + to_version = ( + get_poetry_project_version() + if to_tag is None + else version_string_to_version_info(to_tag.name) + ) + log_msg(f"Project version {to_version} taken from tag: {to_tag is not None}") + + from_commit = None + if args.from_ref is None: + tags_by_name = {strip_leading_v(tag.name): tag for tag in repo.tags} + all_release_version_infos = { + version_info: tags_by_name[tag_name] + for tag_name in tags_by_name + if VersionInfo.isvalid(tag_name) + and (version_info := VersionInfo.parse(tag_name)) + and version_info.prerelease is None + } + log_msg(f"All release versions {all_release_version_infos}") + + versions_before_project_version = [ + version_info for version_info in all_release_version_infos if version_info < to_version + ] + if len(versions_before_project_version) > 0: + highest_version_before_current_version = max(versions_before_project_version) + highest_version_tag = all_release_version_infos[highest_version_before_current_version] + from_commit = highest_version_tag.commit + else: + # No versions before, get the initial commit reachable from to_commit + # from https://stackoverflow.com/a/48232574 + last_element_extractor = deque(repo.iter_commits(to_commit), 1) + from_commit = last_element_extractor.pop() + else: + from_commit = repo.commit(args.from_ref) + + log_msg(f"From commit: {from_commit}") + ancestor_commit = repo.merge_base(to_commit, from_commit) + assert len(ancestor_commit) == 1 + ancestor_commit = ancestor_commit[0] + log_msg(f"Common ancestor: {ancestor_commit}") + + if ancestor_commit != from_commit: + do_not_change_from_ref = args.do_not_change_from_ref and args.from_ref is not None + raise_exception_or_print_warning( + is_error=do_not_change_from_ref, + message_body=( + f"the ancestor {ancestor_commit} for {from_commit} and {to_commit} " + f"is not the same commit as the commit for '--from-ref' {from_commit}." + ), + ) + + ancestor_tag = sha1_to_tags.get(ancestor_commit.hexsha, None) + if ancestor_tag is None: + raise_exception_or_print_warning( + is_error=args.ancestor_must_have_tag, + message_body=( + f"the ancestor {ancestor_commit} for " f"{from_commit} and {to_commit} has no tag" + ), + ) + + ancestor_version_str = ( + None if ancestor_tag is None else str(version_string_to_version_info(ancestor_tag.name)) + ) + + log_msg( + f"Collecting commits from \n{ancestor_commit} " + f"(tag: {ancestor_tag} - parsed version " + f"{str(ancestor_version_str)}) to \n{to_commit} " + f"(tag: {to_tag} - parsed version {str(to_version)})" + ) + + log_dict = generate_changelog(repo, ancestor_commit.hexsha, to_commit.hexsha) + + owner, name = get_repository_owner_and_name() + md_changelog = markdown_changelog( + owner, + name, + str(to_version), + log_dict, + header=True, + previous_version=ancestor_version_str, + ) + + print(md_changelog) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Changelog helper", allow_abbrev=False) + + parser.add_argument("--repo-root", type=str, default=".", help="Path to the repo root") + parser.add_argument( + "--to-ref", + type=str, + help="Specify the git ref-like string (sha1, tag, HEAD~, etc.) that will mark the LAST " + "included commit of the changelog. If this is not specified, the current project version " + "will be used to create a changelog with the current commit as last commit.", + ) + parser.add_argument( + "--from-ref", + type=str, + help="Specify the git ref-like string (sha1, tag, HEAD~, etc.) that will mark the commit " + "BEFORE the first included commit of the changelog. If this is not specified, the most " + "recent actual release tag (no pre-releases) before the '--to-ref' argument will be used. " + "If the tagged commit is not an ancestor of '--to-ref' then the most recent common ancestor" + "(git merge-base) will be used unless '--do-not-change-from-ref' is specified.", + ) + parser.add_argument( + "--ancestor-must-have-tag", + action="store_true", + help="Set if the used ancestor must have a tag associated to it.", + ) + parser.add_argument( + "--to-ref-must-have-tag", + action="store_true", + help="Set if '--to-ref' must have a tag associated to it.", + ) + parser.add_argument( + "--do-not-change-from-ref", + action="store_true", + help="Specify to prevent selecting a different '--from-ref' than the one specified in cli. " + "Will raise an exception if '--from-ref' is not a suitable ancestor for '--to-ref' and " + "would otherwise use the most recent common ancestor (git merge-base) as '--from-ref'.", + ) + + cli_args = parser.parse_args() + main(cli_args) diff --git a/script/make_utils/version_utils.py b/script/make_utils/version_utils.py index 05e0ff3d2..58150f36f 100644 --- a/script/make_utils/version_utils.py +++ b/script/make_utils/version_utils.py @@ -13,9 +13,7 @@ from semver import VersionInfo def strip_leading_v(version_str: str): """Strip leading v of a version which is not SemVer compatible.""" - if version_str and version_str[0] == "v": - return version_str[1:] - return version_str + return version_str[1:] if version_str.startswith("v") else version_str def islatest(args): From 1cc75022516dbb49a5783163192a0ea30781f42c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 16:48:19 +0200 Subject: [PATCH 0378/1104] chore: use Config for docker labels - some configs are duplicated but old ones may not be populated --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 45969264e..3d20fe1a2 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -398,7 +398,7 @@ jobs: run: | SHA1=$(git rev-parse HEAD) ZAMALANG_SHA1=$(docker inspect "${PREFLIGHT_IMAGE}" | \ - jq -rc '.[0].ContainerConfig.Labels["commit-sha"]') + jq -rc '.[0].Config.Labels["commit-sha"]') TAGGED_IMAGE="${BASE_IMAGE}:${ZAMALANG_SHA1}-${SHA1}" docker tag "${PREFLIGHT_IMAGE}" "${LATEST_IMAGE}" docker tag "${PREFLIGHT_IMAGE}" "${TAGGED_IMAGE}" From 6affa54473c919ae937c109fed9bce7b4b4b23dc Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 7 Oct 2021 13:31:12 +0300 Subject: [PATCH 0379/1104] feat(configuration): add option to treat warnings as errors --- .../bounds_measurement/inputset_eval.py | 20 ++++++++++--- concrete/common/compilation/configuration.py | 3 ++ concrete/numpy/compile.py | 9 ++++-- .../bounds_measurement/test_inputset_eval.py | 28 +++++++++++++++++++ tests/numpy/test_compile.py | 14 ++++++++++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index 52cd6c21e..88051b441 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -59,6 +59,7 @@ def _print_input_coherency_warnings( parameters: Dict[str, Any], parameter_index_to_parameter_name: Dict[int, str], get_base_value_for_constant_data_func: Callable[[Any], Any], + treat_warnings_as_errors: bool, ): """Print coherency warning for `input_to_check` against `parameters`. @@ -84,11 +85,20 @@ def _print_input_coherency_warnings( parameters, get_base_value_for_constant_data_func, ) - for problem in problems: - sys.stderr.write( - f"Warning: Input #{current_input_index} (0-indexed) " - f"is not coherent with the hinted parameters ({problem})\n", + messages = [ + ( + f"Input #{current_input_index} (0-indexed) " + f"is not coherent with the hinted parameters ({problem})\n" ) + for problem in problems + ] + + if len(messages) > 0: + if treat_warnings_as_errors: + raise ValueError(", ".join(messages)) + + for message in messages: + sys.stderr.write(f"Warning: {message}") def eval_op_graph_bounds_on_inputset( @@ -161,6 +171,7 @@ def eval_op_graph_bounds_on_inputset( parameters, parameter_index_to_parameter_name, get_base_value_for_constant_data_func, + compilation_configuration.treat_warnings_as_errors, ) first_output = op_graph.evaluate(current_input_data) @@ -184,6 +195,7 @@ def eval_op_graph_bounds_on_inputset( parameters, parameter_index_to_parameter_name, get_base_value_for_constant_data_func, + compilation_configuration.treat_warnings_as_errors, ) current_output = op_graph.evaluate(current_input_data) diff --git a/concrete/common/compilation/configuration.py b/concrete/common/compilation/configuration.py index 07f909e6d..ad3bf1f86 100644 --- a/concrete/common/compilation/configuration.py +++ b/concrete/common/compilation/configuration.py @@ -7,13 +7,16 @@ class CompilationConfiguration: dump_artifacts_on_unexpected_failures: bool enable_topological_optimizations: bool check_every_input_in_inputset: bool + treat_warnings_as_errors: bool def __init__( self, dump_artifacts_on_unexpected_failures: bool = True, enable_topological_optimizations: bool = True, check_every_input_in_inputset: bool = False, + treat_warnings_as_errors: bool = False, ): self.dump_artifacts_on_unexpected_failures = dump_artifacts_on_unexpected_failures self.enable_topological_optimizations = enable_topological_optimizations self.check_every_input_in_inputset = check_every_input_in_inputset + self.treat_warnings_as_errors = treat_warnings_as_errors diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index a5d499996..bf8419715 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -137,12 +137,17 @@ def _compile_numpy_function_into_op_graph_internal( minimum_required_inputset_size = min(inputset_size_upper_limit, 10) if inputset_size < minimum_required_inputset_size: - sys.stderr.write( - f"Warning: Provided inputset contains too few inputs " + message = ( + f"Provided inputset contains too few inputs " f"(it should have had at least {minimum_required_inputset_size} " f"but it only had {inputset_size})\n" ) + if compilation_configuration.treat_warnings_as_errors: + raise ValueError(message) + + sys.stderr.write(f"Warning: {message}") + # Add the bounds as an artifact compilation_artifacts.add_final_operation_graph_bounds(node_bounds) diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index d977fce93..209471873 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -445,3 +445,31 @@ def test_eval_op_graph_bounds_on_non_conformant_numpy_inputset_check_all(capsys) "(expected ClearTensor, shape=(3,)> for parameter `y` " "but got ClearTensor, shape=(3,)> which is not compatible)\n" ) + + +def test_eval_op_graph_bounds_on_non_conformant_inputset_treating_warnings_as_errors(): + """Test function for eval_op_graph_bounds_on_inputset with non conformant inputset and errors""" + + def f(x, y): + return np.dot(x, y) + + x = EncryptedTensor(UnsignedInteger(2), (3,)) + y = ClearTensor(UnsignedInteger(2), (3,)) + + inputset = [ + (np.array([2, 1, 3, 1]), np.array([1, 2, 1, 1])), + (np.array([3, 3, 3]), np.array([3, 3, 5])), + ] + + op_graph = trace_numpy_function(f, {"x": x, "y": y}) + + with pytest.raises(ValueError, match=".* is not coherent with the hinted parameters .*"): + configuration = CompilationConfiguration(treat_warnings_as_errors=True) + eval_op_graph_bounds_on_inputset( + op_graph, + inputset, + compilation_configuration=configuration, + min_func=numpy_min_func, + max_func=numpy_max_func, + get_base_value_for_constant_data_func=get_base_value_for_numpy_or_python_constant_data, + ) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index afa6bb3e1..022b8cfb2 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -333,6 +333,20 @@ def test_small_inputset(): ) +def test_small_inputset_treat_warnings_as_errors(): + """Test function compile_numpy_function_into_op_graph with an unacceptably small inputset""" + with pytest.raises(ValueError, match=".* inputset contains too few inputs .*"): + compile_numpy_function_into_op_graph( + lambda x: x + 42, + {"x": EncryptedScalar(Integer(5, is_signed=False))}, + [(0,), (3,)], + CompilationConfiguration( + dump_artifacts_on_unexpected_failures=False, + treat_warnings_as_errors=True, + ), + ) + + @pytest.mark.parametrize( "function,params,shape,ref_graph_str", [ From 57b3be2f6d34a20847ce4227be43be6a2b743cd5 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 7 Oct 2021 13:31:58 +0300 Subject: [PATCH 0380/1104] fix(benchmarks): treat warnings as errors in benchmarks --- benchmarks/124_minus_x.py | 3 + benchmarks/124_minus_x_tensor.py | 15 +-- benchmarks/common.py | 8 ++ benchmarks/linear_regression.py | 8 +- benchmarks/logistic_regression.py | 32 +++++- benchmarks/single_table_lookup.py | 3 + benchmarks/x_minus_1_2_3.py | 15 +-- benchmarks/x_minus_1_2_3_broadcasted.py | 13 ++- benchmarks/x_minus_24.py | 5 +- benchmarks/x_minus_24_tensor.py | 15 +-- benchmarks/x_minus_y.py | 9 +- benchmarks/x_minus_y_broadcasted_tensors.py | 16 +-- benchmarks/x_minus_y_tensor_and_scalar.py | 17 +-- benchmarks/x_minus_y_tensors.py | 15 +-- benchmarks/x_plus_1_2_3.py | 15 +-- benchmarks/x_plus_1_2_3_broadcasted.py | 15 +-- benchmarks/x_plus_42.py | 5 +- benchmarks/x_plus_42_tensor.py | 15 +-- benchmarks/x_plus_y.py | 5 +- benchmarks/x_plus_y_broadcasted_tensors.py | 14 ++- benchmarks/x_plus_y_tensor_and_scalar.py | 15 +-- benchmarks/x_plus_y_tensors.py | 14 ++- benchmarks/x_times_1_2_3.py | 15 +-- benchmarks/x_times_1_2_3_broadcasted.py | 15 +-- benchmarks/x_times_7.py | 3 + benchmarks/x_times_7_tensor.py | 15 +-- benchmarks/x_times_y.py | 9 +- benchmarks/x_times_y_broadcasted_tensors.py | 14 ++- benchmarks/x_times_y_tensor_and_scalar.py | 15 +-- benchmarks/x_times_y_tensors.py | 14 ++- benchmarks/x_to_the_power_of_2.py | 5 +- .../QuantizedLinearRegression.ipynb | 18 ++-- .../QuantizedLogisticRegression.ipynb | 102 +++++++++++++----- script/progress_tracker_utils/measure.py | 3 +- 34 files changed, 330 insertions(+), 170 deletions(-) create mode 100644 benchmarks/common.py diff --git a/benchmarks/124_minus_x.py b/benchmarks/124_minus_x.py index 8a574e254..1b8e4f13f 100644 --- a/benchmarks/124_minus_x.py +++ b/benchmarks/124_minus_x.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -16,6 +18,7 @@ def main(): function_to_compile, {"x": x}, [(i,) for i in range(2 ** 3)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/124_minus_x_tensor.py b/benchmarks/124_minus_x_tensor.py index 5b5d035f9..ed91bc310 100644 --- a/benchmarks/124_minus_x_tensor.py +++ b/benchmarks/124_minus_x_tensor.py @@ -1,6 +1,7 @@ # Target: 124 - x (Tensor) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(6), shape=(3,)) - inputset = [ - (np.array([36, 50, 24]),), - (np.array([41, 60, 51]),), - (np.array([25, 31, 24]),), - (np.array([34, 47, 27]),), - ] + inputset = [(np.random.randint(0, 2 ** 6, size=(3,)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/common.py b/benchmarks/common.py new file mode 100644 index 000000000..35acb1934 --- /dev/null +++ b/benchmarks/common.py @@ -0,0 +1,8 @@ +import concrete.numpy as hnp + +BENCHMARK_CONFIGURATION = hnp.CompilationConfiguration( + dump_artifacts_on_unexpected_failures=True, + enable_topological_optimizations=True, + check_every_input_in_inputset=True, + treat_warnings_as_errors=True, +) diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index f94413e76..c74b2d0b6 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -5,13 +5,16 @@ # pylint: disable=C0301 import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp def main(): - x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32) - y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32) + x = np.array( + [[69], [130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32 + ) + y = np.array([181, 325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32) class Model: w = None @@ -150,6 +153,7 @@ def main(): function_to_compile, {"x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits))}, inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index dc8bc9bb9..664ac700b 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -2,13 +2,40 @@ import numpy as np import torch +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp def main(): - x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float() - y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float() + x = torch.tensor( + [ + [1, 1], + [1, 1.5], + [1.5, 1.2], + [1, 2], + [2, 1], + [4, 1], + [4, 1.5], + [3.5, 1.8], + [3, 2], + [4, 2], + ] + ).float() + y = torch.tensor( + [ + [0], + [0], + [0], + [0], + [0], + [1], + [1], + [1], + [1], + [1], + ] + ).float() class Model(torch.nn.Module): def __init__(self, n): @@ -218,6 +245,7 @@ def main(): "x_1": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)), }, inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/single_table_lookup.py b/benchmarks/single_table_lookup.py index d52523d67..98f3018a6 100644 --- a/benchmarks/single_table_lookup.py +++ b/benchmarks/single_table_lookup.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -21,6 +23,7 @@ def main(): function_to_compile, {"x": x}, [(i,) for i in range(2 ** input_bits)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/x_minus_1_2_3.py b/benchmarks/x_minus_1_2_3.py index 7e692b735..746f69d39 100644 --- a/benchmarks/x_minus_1_2_3.py +++ b/benchmarks/x_minus_1_2_3.py @@ -1,6 +1,7 @@ # Target: x - [1, 2, 3] import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - inputset = [ - (np.array([6, 2, 4]),), - (np.array([1, 3, 5]),), - (np.array([5, 7, 2]),), - (np.array([1, 7, 7]),), - ] + inputset = [(np.random.randint(0, 2 ** 2, size=(3,)) + np.array([1, 2, 3]),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_minus_1_2_3_broadcasted.py b/benchmarks/x_minus_1_2_3_broadcasted.py index b67e1d138..b78e4a2f4 100644 --- a/benchmarks/x_minus_1_2_3_broadcasted.py +++ b/benchmarks/x_minus_1_2_3_broadcasted.py @@ -1,6 +1,7 @@ # Target: x - [1, 2, 3] (Broadcasted) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -12,14 +13,16 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) inputset = [ - (np.array([[4, 7, 7], [6, 2, 4]]),), - (np.array([[6, 2, 4], [1, 3, 1]]),), - (np.array([[6, 2, 4], [5, 7, 5]]),), - (np.array([[5, 7, 5], [4, 7, 7]]),), + (np.random.randint(0, 2 ** 2, size=(2, 3)) + np.array([1, 2, 3]),) for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_minus_24.py b/benchmarks/x_minus_24.py index 16e711f8a..9a39d0b22 100644 --- a/benchmarks/x_minus_24.py +++ b/benchmarks/x_minus_24.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -15,7 +17,8 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - [(i,) for i in range(2 ** 6)], + [(i,) for i in range(24, 2 ** 6)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/x_minus_24_tensor.py b/benchmarks/x_minus_24_tensor.py index f7ec198cb..b88be1f8d 100644 --- a/benchmarks/x_minus_24_tensor.py +++ b/benchmarks/x_minus_24_tensor.py @@ -1,6 +1,7 @@ # Target: x - 24 (Tensor) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(6), shape=(3,)) - inputset = [ - (np.array([36, 50, 24]),), - (np.array([41, 60, 51]),), - (np.array([25, 31, 24]),), - (np.array([34, 47, 27]),), - ] + inputset = [(np.random.randint(0, 2 ** 5, size=(3,)) + 24,) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_minus_y.py b/benchmarks/x_minus_y.py index ca04b3117..47c24a09e 100644 --- a/benchmarks/x_minus_y.py +++ b/benchmarks/x_minus_y.py @@ -3,6 +3,8 @@ import itertools import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -16,7 +18,12 @@ def main(): inputset = itertools.product(range(4, 8), range(0, 4)) # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_minus_y_broadcasted_tensors.py b/benchmarks/x_minus_y_broadcasted_tensors.py index ea6408350..d586a8ab2 100644 --- a/benchmarks/x_minus_y_broadcasted_tensors.py +++ b/benchmarks/x_minus_y_broadcasted_tensors.py @@ -1,6 +1,7 @@ # Target: x - y (Broadcasted Tensors) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -10,17 +11,20 @@ def main(): return x - y x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(2), shape=(2, 3)) inputset = [ - (np.array([6, 2, 4]), np.array([[5, 1, 3], [0, 0, 4]])), - (np.array([1, 3, 1]), np.array([[0, 3, 1], [1, 2, 1]])), - (np.array([5, 1, 2]), np.array([[5, 0, 2], [2, 1, 1]])), - (np.array([0, 7, 7]), np.array([[0, 5, 1], [0, 7, 2]])), + (np.random.randint(4, 8, size=(3,)), np.random.randint(0, 4, size=(2, 3))) + for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_minus_y_tensor_and_scalar.py b/benchmarks/x_minus_y_tensor_and_scalar.py index 3ab299c65..27e825ca4 100644 --- a/benchmarks/x_minus_y_tensor_and_scalar.py +++ b/benchmarks/x_minus_y_tensor_and_scalar.py @@ -3,6 +3,7 @@ import random import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -12,17 +13,17 @@ def main(): return x - y x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + y = hnp.EncryptedScalar(hnp.UnsignedInteger(2)) - inputset = [ - (np.array([6, 2, 4]), 2), - (np.array([1, 3, 1]), 1), - (np.array([5, 4, 7]), 4), - (np.array([5, 7, 6]), 5), - ] + inputset = [(np.random.randint(4, 8, size=(3,)), random.randint(0, 3)) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_minus_y_tensors.py b/benchmarks/x_minus_y_tensors.py index 6c681e0bc..5932e97c2 100644 --- a/benchmarks/x_minus_y_tensors.py +++ b/benchmarks/x_minus_y_tensors.py @@ -1,6 +1,7 @@ # Target: x - y (Tensors) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -10,17 +11,19 @@ def main(): return x - y x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(2), shape=(3,)) inputset = [ - (np.array([6, 2, 4]), np.array([4, 1, 2])), - (np.array([1, 3, 1]), np.array([1, 1, 0])), - (np.array([5, 1, 2]), np.array([4, 1, 1])), - (np.array([0, 7, 7]), np.array([0, 7, 0])), + (np.random.randint(4, 8, size=(3,)), np.random.randint(0, 4, size=(3,))) for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_plus_1_2_3.py b/benchmarks/x_plus_1_2_3.py index de73fda65..e9339d97f 100644 --- a/benchmarks/x_plus_1_2_3.py +++ b/benchmarks/x_plus_1_2_3.py @@ -1,6 +1,7 @@ # Target: x + [1, 2, 3] import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - inputset = [ - (np.array([6, 2, 4]),), - (np.array([1, 3, 1]),), - (np.array([5, 1, 2]),), - (np.array([0, 7, 7]),), - ] + inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_plus_1_2_3_broadcasted.py b/benchmarks/x_plus_1_2_3_broadcasted.py index 9826714f4..0095cb8c1 100644 --- a/benchmarks/x_plus_1_2_3_broadcasted.py +++ b/benchmarks/x_plus_1_2_3_broadcasted.py @@ -1,6 +1,7 @@ # Target: x + [1, 2, 3] (Broadcasted) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) - inputset = [ - (np.array([[0, 7, 7], [6, 2, 4]]),), - (np.array([[6, 2, 4], [1, 3, 1]]),), - (np.array([[6, 2, 4], [5, 1, 2]]),), - (np.array([[5, 1, 2], [0, 7, 7]]),), - ] + inputset = [(np.random.randint(0, 2 ** 3, size=(2, 3)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index 98a5c1c0a..95f7be9ed 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -15,7 +17,8 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - [(6,), (1,), (5,), (2,)], + [(i,) for i in range(2 ** 3)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/x_plus_42_tensor.py b/benchmarks/x_plus_42_tensor.py index 1829304e8..6ca272380 100644 --- a/benchmarks/x_plus_42_tensor.py +++ b/benchmarks/x_plus_42_tensor.py @@ -1,6 +1,7 @@ # Target: x + 42 (Tensor) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - inputset = [ - (np.array([6, 2, 4]),), - (np.array([1, 3, 1]),), - (np.array([5, 1, 2]),), - (np.array([0, 7, 7]),), - ] + inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_plus_y.py b/benchmarks/x_plus_y.py index 829d1f9dc..a4c809b9c 100644 --- a/benchmarks/x_plus_y.py +++ b/benchmarks/x_plus_y.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -16,7 +18,8 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, - [(6, 1), (1, 4), (5, 3), (2, 0), (7, 7)], + [(random.randint(0, 7), random.randint(0, 7)) for _ in range(32)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/x_plus_y_broadcasted_tensors.py b/benchmarks/x_plus_y_broadcasted_tensors.py index c13aea795..91756ec8b 100644 --- a/benchmarks/x_plus_y_broadcasted_tensors.py +++ b/benchmarks/x_plus_y_broadcasted_tensors.py @@ -1,6 +1,7 @@ # Target: x + y (Broadcasted Tensors) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -13,14 +14,17 @@ def main(): y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) inputset = [ - (np.array([6, 2, 4]), np.array([[5, 1, 2], [0, 7, 7]])), - (np.array([1, 3, 1]), np.array([[0, 7, 7], [6, 2, 4]])), - (np.array([5, 1, 2]), np.array([[6, 2, 4], [1, 3, 1]])), - (np.array([0, 7, 7]), np.array([[1, 3, 1], [5, 1, 2]])), + (np.random.randint(0, 2 ** 3, size=(3,)), np.random.randint(0, 2 ** 3, size=(2, 3))) + for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_plus_y_tensor_and_scalar.py b/benchmarks/x_plus_y_tensor_and_scalar.py index e314a99e9..c44dd9e81 100644 --- a/benchmarks/x_plus_y_tensor_and_scalar.py +++ b/benchmarks/x_plus_y_tensor_and_scalar.py @@ -3,6 +3,7 @@ import random import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -14,15 +15,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) - inputset = [ - (np.array([6, 2, 4]), 4), - (np.array([1, 3, 1]), 1), - (np.array([5, 1, 2]), 2), - (np.array([0, 7, 7]), 5), - ] + inputset = [(np.random.randint(0, 8, size=(3,)), random.randint(0, 7)) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_plus_y_tensors.py b/benchmarks/x_plus_y_tensors.py index 16718cc5e..4508d2ecc 100644 --- a/benchmarks/x_plus_y_tensors.py +++ b/benchmarks/x_plus_y_tensors.py @@ -1,6 +1,7 @@ # Target: x + y (Tensors) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -13,14 +14,17 @@ def main(): y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) inputset = [ - (np.array([6, 2, 4]), np.array([0, 7, 7])), - (np.array([1, 3, 1]), np.array([6, 2, 4])), - (np.array([5, 1, 2]), np.array([1, 3, 1])), - (np.array([0, 7, 7]), np.array([5, 1, 2])), + (np.random.randint(0, 2 ** 3, size=(3,)), np.random.randint(0, 2 ** 3, size=(3,))) + for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_1_2_3.py b/benchmarks/x_times_1_2_3.py index 90812272d..1f3b4bc85 100644 --- a/benchmarks/x_times_1_2_3.py +++ b/benchmarks/x_times_1_2_3.py @@ -1,6 +1,7 @@ # Target: x * [1, 2, 3] import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - inputset = [ - (np.array([6, 2, 4]),), - (np.array([1, 3, 1]),), - (np.array([5, 1, 2]),), - (np.array([0, 7, 7]),), - ] + inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_1_2_3_broadcasted.py b/benchmarks/x_times_1_2_3_broadcasted.py index f6b8cb665..cefe9f8ec 100644 --- a/benchmarks/x_times_1_2_3_broadcasted.py +++ b/benchmarks/x_times_1_2_3_broadcasted.py @@ -1,6 +1,7 @@ # Target: x * [1, 2, 3] (Broadcasted) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) - inputset = [ - (np.array([[0, 7, 7], [6, 2, 4]]),), - (np.array([[6, 2, 4], [1, 3, 1]]),), - (np.array([[6, 2, 4], [5, 1, 2]]),), - (np.array([[5, 1, 2], [0, 7, 7]]),), - ] + inputset = [(np.random.randint(0, 2 ** 3, size=(2, 3)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_7.py b/benchmarks/x_times_7.py index 1faa58112..f5338bfb5 100644 --- a/benchmarks/x_times_7.py +++ b/benchmarks/x_times_7.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -16,6 +18,7 @@ def main(): function_to_compile, {"x": x}, [(i,) for i in range(2 ** 4)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/benchmarks/x_times_7_tensor.py b/benchmarks/x_times_7_tensor.py index c1ad87b73..7798ad568 100644 --- a/benchmarks/x_times_7_tensor.py +++ b/benchmarks/x_times_7_tensor.py @@ -1,6 +1,7 @@ # Target: x * 7 (Tensor) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -11,15 +12,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) - inputset = [ - (np.array([6, 2, 4]),), - (np.array([1, 3, 1]),), - (np.array([5, 1, 2]),), - (np.array([0, 7, 7]),), - ] + inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_y.py b/benchmarks/x_times_y.py index b29c9440c..562289db3 100644 --- a/benchmarks/x_times_y.py +++ b/benchmarks/x_times_y.py @@ -3,6 +3,8 @@ import itertools import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -16,7 +18,12 @@ def main(): inputset = itertools.product(range(4, 8), range(0, 4)) # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_y_broadcasted_tensors.py b/benchmarks/x_times_y_broadcasted_tensors.py index 89a6bf8df..a804ad773 100644 --- a/benchmarks/x_times_y_broadcasted_tensors.py +++ b/benchmarks/x_times_y_broadcasted_tensors.py @@ -1,6 +1,7 @@ # Target: x * y (Broadcasted Tensors) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -13,14 +14,17 @@ def main(): y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 3)) inputset = [ - (np.array([6, 2, 4]), np.array([[5, 1, 2], [0, 7, 7]])), - (np.array([1, 3, 1]), np.array([[0, 7, 7], [6, 2, 4]])), - (np.array([5, 1, 2]), np.array([[6, 2, 4], [1, 3, 1]])), - (np.array([0, 7, 7]), np.array([[1, 3, 1], [5, 1, 2]])), + (np.random.randint(0, 2 ** 3, size=(3,)), np.random.randint(0, 2 ** 3, size=(2, 3))) + for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_y_tensor_and_scalar.py b/benchmarks/x_times_y_tensor_and_scalar.py index 4a13b9947..906c31fa1 100644 --- a/benchmarks/x_times_y_tensor_and_scalar.py +++ b/benchmarks/x_times_y_tensor_and_scalar.py @@ -3,6 +3,7 @@ import random import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -14,15 +15,15 @@ def main(): x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) - inputset = [ - (np.array([6, 2, 4]), 4), - (np.array([1, 3, 1]), 1), - (np.array([5, 1, 2]), 2), - (np.array([0, 7, 7]), 5), - ] + inputset = [(np.random.randint(0, 8, size=(3,)), random.randint(0, 7)) for _ in range(32)] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_times_y_tensors.py b/benchmarks/x_times_y_tensors.py index 118a5d3d9..e4e5976c0 100644 --- a/benchmarks/x_times_y_tensors.py +++ b/benchmarks/x_times_y_tensors.py @@ -1,6 +1,7 @@ # Target: x * y (Tensors) import numpy as np +from common import BENCHMARK_CONFIGURATION import concrete.numpy as hnp @@ -13,14 +14,17 @@ def main(): y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(3,)) inputset = [ - (np.array([6, 2, 4]), np.array([0, 7, 7])), - (np.array([1, 3, 1]), np.array([6, 2, 4])), - (np.array([5, 1, 2]), np.array([1, 3, 1])), - (np.array([0, 7, 7]), np.array([5, 1, 2])), + (np.random.randint(0, 2 ** 3, size=(3,)), np.random.randint(0, 2 ** 3, size=(3,))) + for _ in range(32) ] # Measure: Compilation Time (ms) - engine = hnp.compile_numpy_function(function_to_compile, {"x": x, "y": y}, inputset) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) # Measure: End inputs = [] diff --git a/benchmarks/x_to_the_power_of_2.py b/benchmarks/x_to_the_power_of_2.py index 5e003304a..da3505f64 100644 --- a/benchmarks/x_to_the_power_of_2.py +++ b/benchmarks/x_to_the_power_of_2.py @@ -2,6 +2,8 @@ import random +from common import BENCHMARK_CONFIGURATION + import concrete.numpy as hnp @@ -15,7 +17,8 @@ def main(): engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - [(6,), (1,), (5,), (2,)], + [(i,) for i in range(2 ** 3)], + compilation_configuration=BENCHMARK_CONFIGURATION, ) # Measure: End diff --git a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb index db1b74634..e31e9eb3d 100644 --- a/docs/user/advanced_examples/QuantizedLinearRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLinearRegression.ipynb @@ -64,8 +64,8 @@ "metadata": {}, "outputs": [], "source": [ - "x = np.array([[130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", - "y = np.array([325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" + "x = np.array([[69], [130], [110], [100], [145], [160], [185], [200], [80], [50]], dtype=np.float32)\n", + "y = np.array([181, 325, 295, 268, 400, 420, 500, 520, 220, 120], dtype=np.float32)" ] }, { @@ -95,7 +95,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -201,7 +201,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -233,8 +233,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[2.669915]]\n", - "-3.2335143\n" + "[[2.6698928]]\n", + "-3.2299957\n" ] } ], @@ -517,7 +517,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXcAAAD4CAYAAAAXUaZHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAnZ0lEQVR4nO3deXhU1f3H8feXRUBZooBIQYpU6/KrFjFFUNkXWQVRxArIKqCgMVRFsEik2oKIGCsiKC6oCAiyurQIKopESRBFFgsqKC6sSUjIQkLO74+50SEmECDhTiaf1/PMM3fOvTP5Zp7hw8m5Z8415xwiIhJeyvhdgIiIFD2Fu4hIGFK4i4iEIYW7iEgYUriLiIShcn4XAFCjRg1Xv359v8sQESlREhIS9jrnaua3LyTCvX79+sTHx/tdhohIiWJmOwrap2EZEZEwpHAXEQlDCncRkTCkcBcRCUMKdxGRMKRwFxEJQwp3EZEwpHAXEfFBamo2TZvew6effl8sr69wFxE5xT788DB16vQjLm4yjz/+VrH8DIW7iMgpkpoKI0YcpnnzARw4MJsBA/7JnDlDi+VnhcTyAyIi4erQoUO88cYbxMWl8PLLsH//CmAuY8f+g/HjRxfbz1W4i4gUk6ysLHr0uJk331x4RPtDDz3Egw/+vVh/tsJdRKQYZGdn07z5LcTFLcRsMiNG9CIqCqpVq0CNGjWK/ecr3EVEitj332dz1VV92LlzPnXqTGbZspE0bHhqa9AJVRGRIuIczJx5mD/8oR87d86lY8dH2b791Ac7KNxFRIrE9u3Qvv1hBg8eQFbWbKKj/8lbb91LOZ/GRzQsIyJygpxzbN78P15+OZvYWEdW1mPAy4wf/w/Gji2+mTCFoXAXETkBWVlZdOnyV/773wVHtMfExDB2bPHOhCkMhbuIyHFKT8+mcePefPnlAipW/DuDB19Ks2ZGrVpn06JFC7/LAxTuIiLH5ZNPsunQoQ9JSa/z5z8/zjvv3M0555jfZf2GTqiKiBRCejrcd99hmjbtR1LSXPr1m8T69dEhGeygcBcROaZVq+Cyyw4zaVJ/nJvNuHETePHFewI7nfO3uAIo3EVECnDgANxxB7RocZiffx4EvMIjTZsSM+6+wAHOQXQ0xMT4WWa+FO4iInlkZWXRps2tRERUZdq0qpQrV5XU1JcY36QJY9asCQR6brDHxkJSUsj14HVCVUQkyE8/ZdGkyS189918qlXrR8eOZ1G7NjRs2JBb+/b9NdBjYwNPiIqCKVPAQmvs3VwI/G8TGRnp4uPj/S5DREox5+C117IZOLA3mZnzaN/+cZYsiaZChXwOLBM06JGT41uwm1mCcy4yv30alhGRUu/HH6Fbt8P07n0rmZnzGDlyEv/5TwHBHh19ZFvuEE2IUbiLSKkVWOgLLr74MG++2R94jX/+cwKTJ9+T/8G5QzJRUYEee1RU4HEIBrzG3EWkVPrmG7jtNli5ModatQZx4MArPPLII4wePSr/J5hBRMSRY+xTpgT2RURozD0/GnMXkZPi3JHhmvdxkIyMLDp2HM2qVV9iBnXr7mPHjnjGjx/P2LFji/RnFbeTHnM3s+1mtsHM1ptZvNd2lpktN7Ot3v2ZXruZ2ZNmts3MvjCzRkX3q4iI5BETc+SwyFHmnq9fn0WdOrfw/vuTqVJlH5ddlsQ555Rl8uTJhQt2+G2Qh1iPPdfxjLm3cs41DPpf4n5ghXPuAmCF9xigI3CBdxsCTCuqYkVEjuBcYI558Lh3PnPPDx2CceOyadSoD/v3z6dPn8dJTFzLunVxxMXFMXLkSF9/jeJwMmPu3YCW3vZLwPvAKK99lguM98SZWYSZ1XbO/XQyhYqI/EbwuHeeueeZEybwr4ceYv36H1i1ChITtwIfMG7cJGJiogt8yXBRqDF3M/sWSAQcMN05N8PMkpxzEd5+AxKdcxFmtgyY4Jz7yNu3AhjlnIvP85pDCPTsqVev3hU7duwowl9LREqVPHPPD2VkcH2Pnrz11lKgNmXKGGedVZYxY6KJzjuVsQQ72ph7YXvu1zjnfjCzs4HlZrYleKdzzpnZcZ2Zdc7NAGZA4ITq8TxXROQXeeaeHwJan/cXVv+0AXiaoUNvZ+JEqFbNtwp9Uagxd+fcD979bmAh0BjYZWa1Abz73d7hPwDnBj29rtcmIlK08sw937s7kwurNmf1TxuoUfFfvLdyGM88U/qCHQoR7mZ2hplVyd0G2gNfAkuAft5h/YDF3vYS4FZv1kwTIFnj7SJSLILmni9q8Sjn1ruF7QdW0brucHaMPEzLVqE5k+VUKMywTC1gYWBYnXLAbOfcO2a2FphnZoOAHcBN3vFvAZ2AbUAaMKDIqxYR8ewZHsOdd2Yxt0cfYAHR0VN4fHJUyE5RPFWOGe7OuW+AP+fTvg9ok0+7A4YXSXUiIvnIzMxk4cJFvPfeQWbPhoMH3wTeYMKExxg16m6/ywsJWn5AREqUzMxMunS5kXffXXZE+4QJExg16m8+VRV6FO4iUmJkZBziyitv4osvllG+/JOMHt2N/v2hcuVK1KxZ0+/yQorCXURKhM2bs2je/Gb27l3CH//4FG+/PZwGDfyuKnRpyV8RCWnZ2TBxYhZ/+tMt7N27kL/+9Um2bFGwH4vCXURC1oYN0KRJNvff34ecnPmMG/c4s2ffWdonwhSKhmVEJKQ459i0aRtTpx5mxgwoV248MI9HH53EvfeGz9IBxU3hLiIhIzMzk7Zte/LRR0t/aTt8ODAT5t5787k6khRI4S4iISEx8RCRkb345pulVK06jmHDLuLyy+F3v/sdzZs397u8EkfhLiK++89/sujR42bS0hbTrNlTLFs2nKpV/a6qZNMJVRHxTVISDByYRYcOt5CWtpC77nqSVasU7EVBPXcR8cWiRXD77dns2tUHmM+jj07h3nvv9LussKGeu4icUrt2wU03wfXXZ5OR0Rfn5vHYY49x7713+11aWFHPXUROiYyMTFq1GsAnn7yNc3DaadkkJaUyceJE/vY3rQlT1BTuIlLstm07xNVX38Tu3UuoWbMfHTpU48wzoXHjxvTu3dvv8sKSwl1Eik1ODkydmkV09M0cPryEnj2nMmfOHcGXO5VionAXkSKVnp7OsGHDiIv7jJ07IS0tBdjOuHFPEhNzh9/llRoKdxEpMhkZGXTr1p13312OWWfKli1Po0Zw550x9O/f79gvIEVG4S4iRSKwdEAPVq/+LzCT7t0HMnUq1K7td2Wlk8JdRE5acnImkZE3sG3b21Sp8iwvvDCQG27wu6rSTac1ROSkfPDBIerWvYlt296kadNn2L59sII9BCjcReSEpKbCiBFZtGx5M6mpSxgxYioffzyUs87yuzIBDcuIyHFIT09nwoQJxMfvYtUqSE3dDKxi0qQnuecezYQJJQp3ESmUjIwMOnfuznvvLQfOpmxZqFmzHA8++G9GjBjhd3mSh8JdRI4pMzOTq67qwWef/RezmYwePZCxY6FiRb8rk4Io3EXkqHbsyKRJkxv5+ee3OffcZ1myZCANG/pdlRyLTqiKyJGc++XuuecOcf75Pfn552V07z6Nr78erGAvIRTuIvKrmBiIjmb7t4527bK47bZeZGcv5cHmPVm4cBjly/tdoBSWhmVEBICM9HQWfbKWZe8c4vWnZnKYZcBiYoG7Lv9doCtv5neZUkgKdxEhIyODtu26e0sHAIffxYApwF1RUTBlioK9hNGwjEgpl5KSwWWXXc/q1cs5/fRpTH7sa74GfgbuBgV7CaVwFynF1qzJpE6dG9m69R0iI5/lm6+HMvL7J2kAnJ17UHT0LydZpeRQuIuUQunpcO+9h7jqqp6kpLzJ7bdPZ+2nA6k1IRpiYyEqKnCljaiowGMFfImjMXeRUubDD2HgwCy2besFLGXSpKncc8+QwM6IiECg5w7FTJnya7uGZkoUcyHwv3FkZKSLj4/3uwyRsJWTk8MXX3zLpEk5zJ7tOP30MaSlLeDf/85n6YC8s2I0SyZkmVmCcy4yv33quYuUBCcRuBkZGVxzzfUkJLzzS1taGjzxxBP5rwmT93UV7CVSocPdzMoC8cAPzrkuZnYeMAeoDiQAfZ1zh8ysAjALuALYB/Ryzm0v8spFSouYGEhK+nWoxLnAGHhERGDfUfzwQyaNG/fgxx/foWbNhxg+/A+cfz7Uq1ePZs2anYLixS/H03OPAjYDVb3HE4Epzrk5ZvYMMAiY5t0nOufON7ObveN6FWHNIqWHc4Fgj40NPJ4yJRDsuSc98/TgnXNkZmbiHMybl8WQIX/l0KG36dLlWebPH0yFCv78GuID59wxb0BdYAXQGlgGGLAXKOftbwr8x9v+D9DU2y7nHWdHe/0rrrjCiUgBcnKci4pyLhDlgVtUVKA9SGpqqmvfvr0Djrj9/e/P+FK2FD8g3hWQq4XtuT8B3AdU8R5XB5Kcc9ne451AHW+7DvC99x9Htpkle8fvDX5BMxsCDIHAn4giUoDcWSu5vXf4zReL0tLS6Nq1K++//wGnnXYvOTln0a4dDB/ekM6dO/hQtPjtmOFuZl2A3c65BDNrWVQ/2Dk3A5gBgdkyRfW6ImEnd4w9WHT0LwGfnp5O+/bdWL36fWAWTZv24bnn4Pzz/ShWQkVhvsR0NXCdmW0ncAK1NRALRJhZ7n8OdYEfvO0fgHMBvP3VCJxYFZHjlRvsBXyx6GBqOpdf3p3Vq1dQseKLTJ/eh5UrFexSiJ67c240MBrA67nf45zrbWavAzcSCPx+wGLvKUu8x2u8/Su9sSEROV5mBX6xaF1afVrX7UFy8nL+/OeZLFt2K3Xr+luuhI6Tmec+CphjZg8DnwEzvfaZwMtmtg3YD9x8ciWKlHIxMUfMijmUZTxcZQIPP3kDzr3Dbbc9y/TpAzQdXY5wXOHunHsfeN/b/gZonM8xGUDPIqhNRDwH09IYOnQoa9du5LvvICMjCdjOY49N529/G+x3eRKC9A1VkRCXlpZGp05d+fDDD3CuIxUrlqVx43rcddfD9O7d2+/yJEQp3EVCWHp6Os2bdyMh4X3gZYYM6c2jj0K1an5XJqFO4S4SonbtyiAysjs7d67g7LNfZO7c3rRs6XdVUlJoPXeRELRgQQa///317Ny5nGuvncm3396qYJfjonAXCSF79kCvXpnceOMNZGa+wwMPPMs77wzg9NP9rkxKGg3LiPjs4MGDTJz4KHFxe/nwQ8jM/AL4iKlTp3PHHYP8Lk9KKIW7iI/S0tJo374rH3/8PlCdcuWgRo3yPPzwdIYMGeJ3eVKCKdxFfHLwYDqRkd3YsuUDypd/mYkTe3PXXVC2rN+VSThQuIv4YOPGDJo1605i4gouvvglli3rTYMGflcl4UQnVEVOoexs+Ne/MrjssutJTFxO//7Ps3FjXwW7FDn13EVOkQ0boH//TNatuwF4h8mTZzJyZH+/y5IwpXAXKUbp6eksXLiM11/PZMkSKFfuNeAtnnlmOkOHDvS7PAljCneRYpKWlkbz5l1ISHjvl7asLOPpp59m6FDNhJHipXAXKQZ796bTqNF1fP/9B5x55nNMmtSCFi2gSpUq1KpVy+/ypBRQuIsUgczMTD799FNycnKIj3f8/e//JCNjJW3avMQbb/SlalW/K5TSRuEucpJSUlLo0KEDH3/8cVCrcf/9z/Ovf/X1rS4p3RTuIichNTWVTp06ERf3CVWrTiM19UJ69YL77qtNw4YX+V2elGIKd5ETdPDgQdq370Jc3Bqce43zzuvJzJlwxRV+VyaiLzGJnJCDB9P4y1+6smbNh5Qp8yqPPNKTtWsV7BI61HMXOU7/+186V111Hfv2fcAFF8xiyZJeXKQRGAkx6rmLHINzjqysLDIzs3j88VQuuaQb+/atpE+fF9mypbeCXUKSeu4iR5GSkkL37t1ZuXJlUKsxadLz3HOPZsJI6FK4ixQgNTWVjh07sWbNGsqWvY/y5avSqRMMGfIXrr22PTgHZr8+Ie9jER8p3EXycfDgQVq06My6dWuA1+jRoydTp8I553gHxMRAUhJMmRIIdOcgOhoiIgL7RHymMXeRPPbvT+OSS7qwbt1HVKv2KvPn92TBgqBgdy4Q7LGxgUDPDfbY2EC7cz5WLxKgnrtIkBUr0rjuuq6kpa2iWbNZLFrUi7POynOQWaDHDoFAj40NbEdF/dqTF/GZeu4iQGoq3H57Om3bdiMt7T3uvfdFVq3q/dtgzxUc8LkU7BJC1HOXUislJYUhQ4aQkLCVHTvg0KF9wA6mTXueYcOOMRMmdygmWHS0Al5ChnruUiqlpqbSrl1H5s59na1bz6Z8+XO46qo/MWfOawwb1v/oTw4eY4+KgpycwH3wGLyIz9Rzl1Ln4MGDXHllZzZtiqNMmdcYPbonY8dCxYqFfAGzwKyY4DH23CGaiAj13CUkmAuBXkZkZKSLj4/3uwwpBb7+Oo0mTTqzd+8q6tefzcKFvWjY8ARfTPPcxWdmluCci8xvn4ZlpFRwDp55Jo2LLurK3r2ruOmmWfzvfycR7PDbIFewSwhRuEvY274d2rVL5/bbu5Od/R4TJ77I3Lm9KV/e78pEio/G3CUspaSkMGnSY3zwQSIffww5OQmYreG5555n4ECtCSPh75jhbmYVgVVABe/4+c65cWZ2HjAHqA4kAH2dc4fMrAIwC7gC2Af0cs5tL6b6RX4jNTWVVq06kZCwGoigfHmoXr0CEyfOZMCA/n6XJ3JKFGZYJhNo7Zz7M9AQ6GBmTYCJwBTn3PlAIjDIO34QkOi1T/GOEzklkpIOcumlnUlIWEPlynOZNWs/mZn72b37JwYMGOB3eSKnzDF77i4wnSbVe1jeuzmgNXCL1/4SEANMA7p52wDzgafMzFwoTMuRki/PjJTkpCRefuUV0tPT2bkTZs5cysGDq2nSZDaLFvWkVi0faxXxUaHG3M2sLIGhl/OBqcDXQJJzLts7ZCdQx9uuA3wP4JzLNrNkAkM3e/O85hBgCEC9evVO7reQ0iHPSoxJiYm0v/hi1u7aFXRQRaKjX+bxx3v5VKRIaCjUbBnn3GHnXEOgLtAYOOlrzzjnZjjnIp1zkTVr1jzZl5Nwl2clxgPJyXS45BI+27Wb2qe/AKRy662p7NqVzOOP33KsVxMJe8c1W8Y5l2Rm7wFNgQgzK+f13usCP3iH/QCcC+w0s3JANQInVkVOXNC3QFNiY2kX+2/igRwWUrHWdbz7rNGmjb8lioSSY/bczaymmUV425WAdsBm4D3gRu+wfsBib3uJ9xhv/0qNt0uRMCNl/HiacCafYuQwl7ujrmPDBgW7SF6F6bnXBl7yxt3LAPOcc8vMbBMwx8weBj4DZnrHzwReNrNtwH7g5mKoW0qhHdtTaHxJC3ZzgLpM4nUeowkfwelTAH07VCRYYWbLfAFcnk/7NwTG3/O2ZwA9i6Q6KdUyMjJISEggJ8exckUOj/zjAbJyvuCGP9zHq1/eTYX7d/x6oQwttStyBH1DVUJSUlIS7du3Z+3atUGtZZhw1U2M+uifWolR5BgU7hJyDhw4QIcOHVi3bj2VKk0nO7sB/fvDXXfV5U//d+GvQZ4b8Ap2kd9QuEtISUlJoWXLDqxfn4Bz82ncuBvPPQfnn1/AExTsIvlSuEvISEpKoVGjjnz77adUrDiP2NhuDB4MZbR2qchxU7jLqXGMC1usXZtKmzadSUmJo1GjOSxe3IO6dX2oUyRMqE8kxS8m5shrizqHu/tucsaNIyMjhwceSOXKK7uQkrKaESNeJT7+RgW7yElSz12KV/CyAQBTppB8xx10feYZPgQYPx4AszJMm/Yyw4ZpTRiRoqBwl+IVPGUxNpYDsbG0w0iwcuCiqVKlMl27wsCBV9NGXzMVKTK6QLacGs6RUqYMTanKRtKA+Qwd2o2JE6FaNb+LEymZjnaBbPXcpfg5x87BI4nkD+xiB7WI5bUe39NqmtNURpFiohOqUryc4/XO/+YPz3/MLrbTtetsvrnjW1q9ceeRJ1lFpEip5y7FZs8eGD48jdfffgOI55FHZjNmTE9wN0L5LC0bIFKMFO5SpJKSkhg2bBjx8d+yYwdkZ+/G7DteevFl+t7qzYTRsgEixU7hLkUmOTmZVq2u5fPPP8O51lSrZlx6aXWioyfTo0ePIw9WsIsUK4W7FImkpAM0atSBb79dx2mnzWfChG7cdReULet3ZSKlk8JdTsi+ffsYM2YM+/btIzUVPvxwM2lpX3HppfNYtKgbDRr4XaFI6aZwl+O2f/9+2rZty6ZNmzjzzAvYvRvMTuOOO17nqaeu14iLSAhQuMtxSUxMpF27dmzcuIn69RezdWsHrrsOpk2D3/3O7+pEJJfCXQotOTmZdu3a8/nnX+LcQpKSOjBnDtx0k86PioQahbsUyoEDB7j66mvZuPFzYAG9e3fiiSegRg2/KxOR/Cjc5Zh+/jmFRo068NNPCVSvPp9Zs7rSqZPfVYnI0Sjc5TcSExOZPXs2mZmZbN0KL7wwn8zMT7n22nnMm9eNqlX9rlBEjkXhLkfYv38/bdq0Yf369b+0mVXkoYfm8OCDPQp+ooiEFIW7/OLXmTCbOfPMZSQnN+POO2HcuNM488yKfpcnIsdB4S5AYE2YVq3as2HDl+TkLKRevU7MnAlXXOF3ZSJyIrTkb7jKu5TuUZbWTUpK5oorruXzzz+nTJkFPPxwJ9auVbCLlGTquYejmJjAdUtzV150LrB2ekREYF+QjRsPcPXVHUhOXseFF85n4cIuXHyxDzWLSJFSzz3cBF+QOvdiGNHRgcdJSWSkpxMXF8fq1Wu4556PueyyjiQnxzNo0Dw2buymYBcJE+q5h5s8F6QmNjawHRXF/rFjaXPVVXlmwpTl6afncvvt15/6WkWk2OgC2eHKOSjz6x9mifv20bpNW778chMwlQoV6jJ0KAwe/Hsuvvgi/+oUkROmC2SXNrlDMZ4k4OoLLmNL4h6cW8T113dk6lSoXdu3CkWkmGnMPdwEj7FHRbHrp0T+74wL2bx/N9VOe5n5r3fgjTcU7CLhTj33cGMWmBUTFcV/Oz5Et/M6kJHxNa3q/pP5t3zLWTdq+UaR0kDhHoZS74nhnr8dYHqHjkA8Y8fOY/xD3bUur0gponAPE/v376dLly6sWbPmlzazssyaNYc+fTQTRqS0OWa4m9m5wCygFuCAGc65WDM7C5gL1Ae2Azc55xLNzIBYoBOQBvR3zq0rnvIFAmvCtGqVOxPmPqpXr8h110Hfvi1p1aqV3+WJiA8K03PPBv7mnFtnZlWABDNbDvQHVjjnJpjZ/cD9wCigI3CBd7sSmObdSzFISkoiMrId33yzkTJlFjF6dEcefBAqap0vkVLtmOHunPsJ+MnbTjGzzUAdoBvQ0jvsJeB9AuHeDZjlAhPo48wswsxqe68jJ2n37t3cfPPNbN26lZwc2LMnlaysgzRo8AYLFnSkYUO/KxSRUHBcUyHNrD5wOfAJUCsosH8mMGwDgeD/PuhpO722vK81xMzizSx+z549x1t3qbRnzx5at25NXFwc9eu3Ze/e9hw+fAODB7/Nli1dFOwi8otCn1A1s8rAAuBu59wBC5p54ZxzZnZcX3V1zs0AZkDgG6rH89zSaO/evbRp04Zt277mT396k48+as0118Bzz8GFF/pdnYiEmkL13M2sPIFgf9U594bXvMvManv7awO7vfYfgHODnl7Xa5MTtG/fPtq2bcuWLVsxW8pXX7Xmqafggw8U7CKSv2OGuzf7ZSaw2Tn3eNCuJUA/b7sfsDio/VYLaAIka7z9xCUmJtKsWTu++GILWVmLaNmyLRs3wvDhRywdIyJyhMIMy1wN9AU2mNl6r20MMAGYZ2aDgB3ATd6+twhMg9xGYCrkgKIsOGw5d+SXjJxjz95kGjZsx48/bqRy5cU8/fS19Omj7yKJyLEVZrbMR0BBcdImn+MdMPwk6ypdvItr7B49mrEPPkhSUhKJa75i1Y9pZB7ezjXXLGTBgg6cfbbfhYpISaFvqPrNu7jGnthYWr/yCttSUznDzmZ/xhmUK1OBMWMW8Mgjnf2uUkRKGI3a+s2MvQ88QJvq1dm6L5EamdPZn/Edg//vefbs/YJHHunqd4UiUgKp5+6z5ORkWrdpx6akgxzmP5xGA96lDW02vKvBdRE5Yeq5+6xnz2g2bPiSw4ff4G42sIFLacPKX69/KiJyAhTuPtm3D9q0+Q/Ll79A9Yq3s4aHmBK1gzNyUiEq6sgLXIuIHCcNy5xizsH8+XDHHQfYu/c2qle/iK8Hn0m1jCaBC1sHX+A6IkJDMyJyQhTup9CPP8Idd8DixVCz5ijMdrJ06WqqNW165Dz33IBXsIvICVK4nwLOwZAhr/L88+PIyckiIgL27PmOkSNH0rRp08BBeYNcwS4iJ0HhXsy++Qa6dn2VTZv6UrlyI9q3v5SqVeGcc85h7NixfpcnImFK4V5MDh+Gf/8bRo16jUOHbuXCC1sSH7+MypVP97s0ESkFFO7FYONGGDQIPvlkHtCHJk2a8e67SznjDAW7iJwamgpZhA4dgvHj4fLLYdOm+ZQpcwvXXHMVy5cv44wzzvC7PBEpRRTuRWTtWoiMhHHj4MorF5Ke/leaNm3CW2+9ReXKlf0uT0RKGQ3LnKS0NLjllkUsXjyXSpXg6qsPExe3kL/85S+89dZbVKlSxe8SRaQUUrifhPffh169XmH37lupVKkWdepUZc8e6Ny5My+99BJVq1b1u0QRKaUU7icgORlGjYLp02cD/WjYsCWrVy/j9NN1wlREQoPC/TgsWbKEF1/8kOXLITX1IGbTueaaZrz99lIFu4iEFIV7IU2Z8hwjR94GVMCsLBUrQqtW1zJv3jzNhBGRkKNwPwbnYNiwF5gxYwhmHRgzZiEPPliR007zuzIRkYIp3I9i507o2nUW69cPolq1dqxcuZBGjSr6XZaIyDFpnns+cnJg+nS44IJXWL++P3/8Yxt27lykYBeREkPhnse2bdCmDQwbNpuMjH40bdqKzz5bTOXKlfwuTUSk0DQs4/nkkwSefvo7XnsNypb9FrN7adGiOcuWLdFMGBEpcRTuQEzMczz00G2/PM7KgmbNmrF06VLNhBGREqlUh3tmJvTs+QJLlw6hfPkO/OMf/+Laa40yZYxLLrmEcuVK9dsjIiVYqU2vuDi44YaX+PHHQdSu3Y61axdSp07FIy93JyJSQpW6E6oHD0J0NDRt+go//jiAhjUu5OttQcEeHQ0xMX6XKSJyUkpVuK9YAZdeCk88MRuzfjT73bms3ruFSmPG/BrssbGQlBR4LCJSQpWKcN+xI5lbbvmZtm1/Ji3tFcqU6UuLFs15+6uNnB4VFQj0MmUC91FRMGWKhmZEpEQzFwI91MjISBcfH18srz1ixEymTh0KHP6lrVmzZrz99tuBmTDOBYI9V06Ogl1ESgQzS3DORea3L2xPqO7aBV26vEB8/G1UrtyWESN68PvfQ6VKlbjxxht/Dfbo6COfGB2tnruIlHhhF+7OwSuvwO23v8TBg4O44IL2JCQsokqVir89MHeMPXcoJvcxKOBFpEQLq3D/7jsYOhTeeecVYABNm7ZhxYqFVKqUz5owZhARceQY+5QpgX0REQp2ESnRwmLMPScHnnkmcHWkrKzZHDrUl5YtW7JsWSEuopF3XrvmuYtICXG0MfdjzpYxs+fNbLeZfRnUdpaZLTezrd79mV67mdmTZrbNzL4ws0ZF92vk76uvoEULGD4c6tefS1ZWX29NmEJeHSlvkCvYRSQMFGYq5ItAhzxt9wMrnHMXACu8xwAdgQu82xBgWtGUmb9+/WZw0UUN+PjjBtSs2YDNm3tz9dVXs2yZrmcqIqXbMcfcnXOrzKx+nuZuQEtv+yXgfWCU1z7LBcZ64swswsxqO+d+KrKKg/zf/9WhXr1raNwYKlWCs88+m3HjxmmxLxEp9U70hGqtoMD+GajlbdcBvg86bqfX9ptwN7MhBHr31KtX74SKuO++ztx3X+cTeq6ISDg76W+oer304z4r65yb4ZyLdM5F1qxZ82TLEBGRICca7rvMrDaAd7/ba/8BODfouLpem4iInEInGu5LgH7edj9gcVD7rd6smSZAcnGNt4uISMGOOeZuZq8ROHlaw8x2AuOACcA8MxsE7ABu8g5/C+gEbAPSgAHFULOIiBxDYWbL/LWAXW3yOdYBw0+2KBEROTmlYslfEZHSRuEuIhKGFO4iImEoJBYOM7M9BE7M+qkGsNfnGo6Xai5+Ja1eUM2nSijU/HvnXL5fFAqJcA8FZhZf0OpqoUo1F7+SVi+o5lMl1GvWsIyISBhSuIuIhCGF+69m+F3ACVDNxa+k1Quq+VQJ6Zo15i4iEobUcxcRCUMKdxGRMFRqw93MtpvZBjNbb2bxXlu+14b1m5ld6NWZeztgZnebWYyZ/RDU3snnOkP6ervHUfMkM9vi1bXQzCK89vpmlh70fj8TQjUX+Fkws9He+/yVmV0bQjXPDap3u5mt99p9f5/N7Fwze8/MNpnZRjOL8tpD+vN8BOdcqbwB24EaedoeBe73tu8HJvpdZz51lyVw9avfAzHAPX7XFFRbc6AR8OWx3lMCq4e+DRjQBPgkhGpuD5TzticG1Vw/+LgQe5/z/SwAlwCfAxWA84CvgbKhUHOe/ZOBB0PlfQZqA4287SrA/7z3MqQ/z8G3UttzL0A3AteExbvv7l8pBWoDfO2c8/sbvb/hnFsF7M/TXNB7+sv1dp1zcUBE7gVgTqX8anbO/dc5l+09jCNw0ZmQUcD7XJBuwBznXKZz7lsCy3E3LrbiCnC0ms3MCCwb/topLeoonHM/OefWedspwGYClwwN6c9zsNIc7g74r5kleNdzhYKvDRtKbubIfwQjvD8Dnw+VYaQ8jvd6u6FmIIEeWa7zzOwzM/vAzJr5VVQB8vsslIT3uRmwyzm3NagtZN5nM6sPXA58Qgn6PJfmcL/GOdcI6AgMN7PmwTtd4G+tkJonamanAdcBr3tN04A/AA0JXIR8sj+VFU4ovqdHY2YPANnAq17TT0A959zlwEhgtplV9au+PErUZyGPv3JkhyVk3mczqwwsAO52zh0I3hfqn+dSG+7OuR+8+93AQgJ/qhZ0bdhQ0RFY55zbBeCc2+WcO+ycywGexYc/twuhRF5v18z6A12A3t4/YryhjX3edgKB8es/+lZkkKN8FkL9fS4H9ADm5raFyvtsZuUJBPurzrk3vOYS83kuleFuZmeYWZXcbQIn0L6k4GvDhoojejh5xvSuJ/A7hJoSd71dM+sA3Adc55xLC2qvaWZlve0GwAXAN/5UeaSjfBaWADebWQUzO49AzZ+e6vqOoi2wxTm3M7chFN5n7zzATGCzc+7xoF0l5/Ps9xldP25AAwIzCD4HNgIPeO3VgRXAVuBd4Cy/aw2q+QxgH1AtqO1lYAPwBYEPV22fa3yNwJ/UWQTGHAcV9J4SmFUwlUCvbAMQGUI1byMwfrreuz3jHXuD93lZD6wDuoZQzQV+FoAHvPf5K6BjqNTstb8IDMtzrO/vM3ANgSGXL4I+B51C/fMcfNPyAyIiYahUDsuIiIQ7hbuISBhSuIuIhCGFu4hIGFK4i4iEIYW7iEgYUriLiISh/wcnkJcMBAKRuwAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -694,7 +694,7 @@ "data": { "image/png": "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smULZ8+e1WLUmId25ihWr17NlVdeSRsPpUT3NnoGjh8T7DNz7rjjDqKjo8nIyDDbFHegZ+D4M8HszCkpKWHdunUB00QF3Uz1a4LZmfPDDz+Qm5urxajxHYLVmZOZmUmDBg1o27at2aa4DS1GPydYnTmZmZn06NGDkJDAeYQD506CmGCbmVNaWsratWsDqokKWowBQzA5c7Zt28apU6f8fmV/ebQYA4RgcuZkZmZSv359UlJSzDbFrWgxBhDB4syx9RcDbWxVizGACAZnjogEZH8RtBgDDk84c7Zu3Urfvn2Ji4sjNjaW3r17s27dOrd8d03ZsWMHv/zyS5X9xaVLl5KUlERYmP9EI9ViDEDc6czZuHEj3bp1IzY2ll27drF//36aN29OWloay5cvd4O1NSMzM5N69erRoUMHl8f37dtH//79ef755zl58qR3jaslem5qgPLss88yc+ZMfvrpp0sOgFxaWkr79u3Jyclh3759REdHA2oqWtu2bblw4QLZ2dlERka60/RKufvuuzl//jxLlixxefzee++lffv2PPPMMzRt2pQTJ05QXFxROhufQs9NDVTc4czJyspix44dDB48uEyIAKGhoQwbNozDhw+zePFid5hbLUSErKysSvuLM2fOZMKECX7VPLWhxRiguMOZs2rVKgCuv/56p2O2fStXrrx0I2vI7t27OXnyZKVitP/R8De0GAOY2jpzdu/eDcBVV13ldCwhIQGAPXv21M7IGpCZmUlMTAzXXXed167pTbQYA5zaOHNyc3MBqFvXOUdTTEwMAKdPn66VfTUhMzOT7t27Ex4e7rVrehMtxgDHUzNzbH4/bwaCqqq/6O9oMQYBl+rMiYuLA+D8+fNOx2z7bOd4mj179nDs2DEtRo1/c6nOnNatWwNw5MgRp2NHjx4F8Fq80szMTOrUqePSmRQoaDEGCZfizLHlr9i8ebPTMdu+Xr16uc/ISsjMzKRbt25ERER45XpmoMUYRNicOdOnT6/W+T179iQ5OZn58+eTn59ftr+kpIR58+aRmJhI3759PWWuA2vWrAnoJipoMQYVycnJPPnkk0ycOLFazpyQkBBmzpxJTk4Oo0eP5sSJE5w6dYpx48aRnZ3NjBkziIqK8rjd+/bt49ChQ1qMmsCips6crl27sn79es6cOUOrVq1o2rQp2dnZrF69mt///vcetlaRmZlJVFQUnTt3rvLcxYsXY7FYsFgsHD16lJKSkrLPH374oResvXT03NQgZN68eQwfPpx169bRtWtXs82pklGjRnH48OGyGUEBip6bGoz4W8wcW3KbQEeLMUipqTPHLA4dOsTBgwe1GDWBS02dOWbxzTffEBkZSZcuXcw2xeNoMQYx/hAzJzMzky5duvj1aozqosUYxMTExPh8zJxg6S+C9qZq8N1sVkeOHCExMZGvv/7aazN9TER7UzW+68xZvXo1ERERfjH84g60GDU+68zJzMykc+fOLtdTBiJajBrAN505wdRfBC1GjRV7Z85///tfs83h+PHjZGdnB5UYtQNH44CvOHPmzp3LyJEjycnJITY21jQ7vIh24GgcmTp1qk84czIzM7n++uuDRYiAbqZqytGmTRufcOYEW38RtBg1LvCmM+f06dO89NJLfP3112VxdX755Rd++umnoBOj7jNqXGJbZrV27VpuvPFGp+MlJSVu6VMWFBQQHR2NiBAaGsp1111HQkIC//73vzl8+DBNmjSp9TX8hAwtRk2FuHLm7N+/nyeffJKRI0cyaNAgt1ynXr165OXllX0ODw+nqKiI0NBQ2rdvz+9+9zt69uxJampqIPchMxCNpgJ27twp4eHh8t5778nFixflL3/5i0RERAggzz33nNuu06JFCwEqfIWHhwsgf/vb39x2TR8kXdeMmkr505/+xCeffEKdOnU4ePBg2WLk1NRUsrKy3HKNnj17VvpdYWFhXHvttWzcuNGn5s66mQz/S9Wj8RpHjhzhwIEDnDhxgpCQEEpLS8uOff/995SWlhISUnsfYGJiotP322OxWJg9e3YgCxHQ3lSNCwoLC3n33Xdp2bIlCxcuBHASyvnz58nOznbL9Ro3blxh/ozQ0FBeeeUV2rZt65Zr+TK6ZtQ4cPDgQW6++WYOHDhAZT2YkJAQvvvuO1q1alXrazZq1MjltcLCwkhKSuLZZ5+t9TX8AV0zahy4+uqr+dOf/kRISEilzcKwsDC+++47t1yzcePGLrMLiwhz5swJ2KxT5dFi1DgxduxYvvnmG2JjYyvMAFxYWMi6devccr34+HinZnBoaCjPP/88nTp1css1/AHtTdVUyL59++jTpw8HDhygqKjI6XhERATnzp2rdc21fft2UlJSyj6HhYVx9dVXs337dq9ELPcR9ERxTcW0aNGC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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -746,7 +746,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] diff --git a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb index 61fa0b204..ee48a14e4 100644 --- a/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb +++ b/docs/user/advanced_examples/QuantizedLogisticRegression.ipynb @@ -65,8 +65,34 @@ "metadata": {}, "outputs": [], "source": [ - "x = torch.tensor([[1, 1], [1, 2], [2, 1], [4, 1], [3, 2], [4, 2]]).float()\n", - "y = torch.tensor([[0], [0], [0], [1], [1], [1]]).float()" + "x = torch.tensor(\n", + " [\n", + " [1, 1],\n", + " [1, 1.5],\n", + " [1.5, 1.2],\n", + " [1, 2],\n", + " [2, 1],\n", + " [4, 1],\n", + " [4, 1.5],\n", + " [3.5, 1.8],\n", + " [3, 2],\n", + " [4, 2],\n", + " ]\n", + ").float()\n", + "y = torch.tensor(\n", + " [\n", + " [0],\n", + " [0],\n", + " [0],\n", + " [0],\n", + " [0],\n", + " [1],\n", + " [1],\n", + " [1],\n", + " [1],\n", + " [1],\n", + " ]\n", + ").float()" ] }, { @@ -96,7 +122,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -175,22 +201,42 @@ "name": "stdout", "output_type": "stream", "text": [ - "Epoch: 1 | Loss: 0.9555807113647461\n", - "Epoch: 101 | Loss: 0.12865914404392242\n", - "Epoch: 201 | Loss: 0.0773993730545044\n", - "Epoch: 301 | Loss: 0.05493553355336189\n", - "Epoch: 401 | Loss: 0.042454302310943604\n", - "Epoch: 501 | Loss: 0.034546975046396255\n", - "Epoch: 601 | Loss: 0.02910044603049755\n", - "Epoch: 701 | Loss: 0.02512541227042675\n", - "Epoch: 801 | Loss: 0.02209884487092495\n", - "Epoch: 901 | Loss: 0.019718701019883156\n", - "Epoch: 1001 | Loss: 0.017798559740185738\n", - "Epoch: 1101 | Loss: 0.016217226162552834\n", - "Epoch: 1201 | Loss: 0.014892562292516232\n", - "Epoch: 1301 | Loss: 0.013766967691481113\n", - "Epoch: 1401 | Loss: 0.012798796407878399\n", - "Epoch: 1501 | Loss: 0.011957273818552494\n" + "Epoch: 1 | Loss: 0.693336546421051\n", + "Epoch: 101 | Loss: 0.1125209778547287\n", + "Epoch: 201 | Loss: 0.07049673795700073\n", + "Epoch: 301 | Loss: 0.050856731832027435\n", + "Epoch: 401 | Loss: 0.039525073021650314\n", + "Epoch: 501 | Loss: 0.0322115495800972\n", + "Epoch: 601 | Loss: 0.027129750698804855\n", + "Epoch: 701 | Loss: 0.023406751453876495\n", + "Epoch: 801 | Loss: 0.02056846395134926\n", + "Epoch: 901 | Loss: 0.018336370587348938\n", + "Epoch: 1001 | Loss: 0.01653693988919258\n", + "Epoch: 1101 | Loss: 0.015056520700454712\n", + "Epoch: 1201 | Loss: 0.013817812316119671\n", + "Epoch: 1301 | Loss: 0.012766523286700249\n", + "Epoch: 1401 | Loss: 0.01186333317309618\n", + "Epoch: 1501 | Loss: 0.011079175397753716\n", + "Epoch: 1601 | Loss: 0.010392050258815289\n", + "Epoch: 1701 | Loss: 0.009785104542970657\n", + "Epoch: 1801 | Loss: 0.009245104156434536\n", + "Epoch: 1901 | Loss: 0.008761593140661716\n", + "Epoch: 2001 | Loss: 0.00832616537809372\n", + "Epoch: 2101 | Loss: 0.007932038977742195\n", + "Epoch: 2201 | Loss: 0.007573576178401709\n", + "Epoch: 2301 | Loss: 0.007246167398989201\n", + "Epoch: 2401 | Loss: 0.006945951841771603\n", + "Epoch: 2501 | Loss: 0.006669704802334309\n", + "Epoch: 2601 | Loss: 0.006414605770260096\n", + "Epoch: 2701 | Loss: 0.006178391166031361\n", + "Epoch: 2801 | Loss: 0.00595900509506464\n", + "Epoch: 2901 | Loss: 0.005754708778113127\n", + "Epoch: 3001 | Loss: 0.005564006045460701\n", + "Epoch: 3101 | Loss: 0.0053855921141803265\n", + "Epoch: 3201 | Loss: 0.005218283273279667\n", + "Epoch: 3301 | Loss: 0.005061114672571421\n", + "Epoch: 3401 | Loss: 0.004913175944238901\n", + "Epoch: 3501 | Loss: 0.004773670341819525\n" ] } ], @@ -200,7 +246,7 @@ "optimizer = torch.optim.SGD(model.parameters(), lr=1)\n", "criterion = torch.nn.BCELoss()\n", "\n", - "epochs = 1501\n", + "epochs = 3501\n", "for e in range(1, epochs + 1):\n", " optimizer.zero_grad()\n", "\n", @@ -253,7 +299,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -291,9 +337,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.54124975]\n", - " [2.37628269]]\n", - "-14.683035850524902\n" + "[[5.28773165]\n", + " [2.6065383 ]]\n", + "-16.89045524597168\n" ] } ], @@ -603,7 +649,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -807,7 +853,7 @@ "data": { "image/png": "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cz2TijvbU0aWLl3K7NmzGTt2rE4J5kE02WXDJ598QlJSEq+88kq2/22hQoUAMlyR2f6c/T2utHv3bi5duqTJTnmkevXqZWn4gae0p+tdvXqVQYMG0aVLF1q3bu32/avMabLLhiJFijB69GgmTpzIunXrsvVvK1euDJBhJ5cTJ04AEBkZmfsgb2Lz5s3kyZOHatWquXxfSmVXvXr1+PPPP2969cRT2tP1Xn31VS5evMjo0aPdvm91Y5rssqlr1660atWKp556KltTid13330AbN26Nd1r9ueaN2/unCBvYMuWLdx9993azVl5pPr16yMiGbaT1DylPaW2ZcsWxo4dy8cff0zx4sXdum91czYRH52MzoWOHj1K1apVefHFF7O8JlVSUhLVq1fnwoULHDx4kLCwMAASExOpXr060dHR7Nu3L+V5V6lVqxbNmzfn448/zva/LV26NKdOnSIhIcEFkSlllClThmeeeeaGtws8pT3ZJSQkUK9ePQoUKMDKlStzfL9Q25jLzNUzuxy47bbbePPNN3n33XfZs2dPlv5NQEAAU6ZMISoqit69e3Pq1CnOnTvHoEGD2L9/P5MmTXJ5w7x27Rq7d+/WyZ+VR6tXr95NO6l4QntK7eOPP2bPnj2MHz/eso4x6sY02eXQ0KFDqVGjBn379iUpKSlL/6ZBgwZs2LCBixcvUqlSJcqWLcv+/ftZtWoVDz30kIsjNpd34uPjs9U5ZfHixdhsNmw2GydOnCAxMTHl8eTJk10YrfJX9erVY+PGjTd9n9Xtye7IkSO88847jBw5MuVeYnZoG3MPvYyZC3/88Qd16tRhzJgx9O/f3+pwburTTz/l/fff58yZM1aHolSmVq5cyf3338+xY8cyHDTuaR588EFOnTrF1q1bbzhTirKUXsbMjRo1avDcc8/x0ksvpfQA82RZXelAKSvVqVOHwMBAr1jMddq0aSxfvvymU4Ip62myy6U333yTokWLMnToUKtDualNmzZpslMeL3/+/FSuXDnXg8td7ezZs7z00ksMHjyYe+65x+pw1E1ossul8PBwJk2axIIFC1i4cKHV4WTqn3/+4fDhw5rslFeoX79+tta2s8Jzzz1HWFgY77zzjtWhqCzQZOcE9913H926deOZZ55x+lRizrJ582ZsNpv2xFReoW7duvz2228kJiZaHUqGfv75Z7755hvG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lFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK52myU0op5fM02SmllPJ5muyUUkr5PE12Sqmb2rFjB61ataJQoULkz5+fFi1asH79eqvDUirLNNkppW5o06ZNNGzYkPz587N3714OHz5M+fLladasGb/88ovV4SmVJTYREauDUEpZY86cOXTq1InM/gwkJSVRo0YNoqKiOHjwIOHh4QAkJiZStWpVrl69yv79+wkNDXVn2Epl11w9s1NKZWrNmjXs3r2bJ554IiXRAQQGBtKlSxeOHTvG4sWLLYxQqazRZKeUytSKFSsAqFOnTrrX7M8tX77crTEplROa7JRSmfrzzz8BKF26dLrXIiIiANi3b59bY1IqJzTZKaUydeHCBQDy5s2b7rV8+fIBcP78eXeGpFSOaLJTSuWIvVOLzWazOBKlbk6TnVIqU4UKFQLgypUr6V6zP2d/j1KeTJOdUipTlStXBuD48ePpXjtx4gQAkZGRbo1JqZzQZKeUytR9990HwNatW9O9Zn+uefPmbo1JqZzQZKeUylTTpk2pUqUK8+bNIyYmJuX5xMREZs2aRZkyZWjVqpWFESqVNZrslFKZCggIYMqUKURFRdG7d29OnTrFuXPnGDRoEPv372fSpEmEhYVZHaZSN6XJTil1Qw0aNGDDhg1cvHiRSpUqUbZsWfbv38+qVat46KGHrA5PqSwJsjoApZTnq1WrFkuWLLE6DKVyTM/slFJK+TxNdkoppXyeJjullFI+T5OdUkopn6fJTimllM/TZKeUUsrnabJTSinl8zTZKaWU8nma7JRSSvk8TXZKKaV8niY7pZRSPk+TnVJKKZ+nyU4ppZTP02SnlFLK5+kSP0r5iRMnTlC9enXi4+PTPJ8nTx7y58+f8thms3HPPffw888/uztEpVxGk51SfiIiIoI77riD3377DRHJ9H02m42WLVu6MTKlXE8vYyrlR3r06EFAwM2bfYcOHdwQjVLuo8lOKT/SqVOnG74eEBBAkyZNiIiIcFNESrmHJjul/EjRokVp1qwZgYGBGb5us9no3r27m6NSyvU02SnlZ7p3757pPTubzcZjjz3m5oiUcj1Ndkr5mccee4ygoPR904KCgmjZsiVFihSxICql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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -838,7 +884,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5385b79125e44bc493c7eaf5f8013b14", + "model_id": "7de68790a0584e96aa7e8e30a535275a", "version_major": 2, "version_minor": 0 }, @@ -878,7 +924,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 006713d0b..17d466720 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -261,7 +261,8 @@ def main(): scripts = list(base.glob("*.py")) # Create a directory to store temporary scripts - os.makedirs(".benchmarks/scripts", exist_ok=True) + shutil.rmtree(".benchmarks/scripts", ignore_errors=True) + shutil.copytree(base, ".benchmarks/scripts") # Process each script under the base directory for path in scripts: From f2c08571a3e6226f60912eaa66e2d2da4869acb8 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 17:29:30 +0200 Subject: [PATCH 0381/1104] chore: add changelog generation and artifact upload for release --- .github/workflows/continuous-integration.yaml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 3d20fe1a2..c07109a89 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -235,6 +235,17 @@ jobs: if: ${{ steps.install-deps.outcome == 'success' && !cancelled() }} run: | make docs + - name: Generate release changelog + id: changelog + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && steps.install-deps.outcome == 'success' && !cancelled() }} + run: | + GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') + CHANGELOG_FILE="CHANGELOG_${GIT_TAG}.md" + echo "::set-output name=changelog-file::${CHANGELOG_FILE}" + poetry run python ./script/make_utils/changelog_helper.py \ + --to-ref "${GIT_TAG}" \ + --to-ref-must-have-tag \ + --ancestor-must-have-tag > "${CHANGELOG_FILE}" - name: Conformance status id: conformance if: ${{ always() && !cancelled() }} @@ -245,13 +256,18 @@ jobs: echo "Conformance failed, check logs" exit 1 fi - - name: Archive docs artifacts if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 with: name: html-docs path: docs/_build/html + - name: Upload changelog artifacts + if: ${{ steps.changelog.outcome == 'success' && !cancelled() }} + uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 + with: + name: changelog + path: ${{ steps.changelog.outputs.changelog-file }} - name: PyTest Source Code id: pytest if: ${{ steps.conformance.outcome == 'success' && !cancelled() }} From f443b41cef429e3901bcf75023b878cac43e93e6 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 8 Oct 2021 10:52:29 +0300 Subject: [PATCH 0382/1104] docs(benchmarks): explain the module trick used in the measurement script with comments --- script/progress_tracker_utils/measure.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 17d466720..7c88c2cf3 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -260,10 +260,17 @@ def main(): result = {"machine": machine, "metrics": {}, "targets": {}} scripts = list(base.glob("*.py")) - # Create a directory to store temporary scripts + # Clear the previous temporary scripts directory shutil.rmtree(".benchmarks/scripts", ignore_errors=True) + + # Copy the base directory to the new temporary scripts directory shutil.copytree(base, ".benchmarks/scripts") + # Because we copy the entire base directory to the new temporary scripts directory, + # the modified scripts will have access to helper modules defined within the base directory + # (e.g., we copy `benchmarks/common.py` to `.benchmarks/scripts/common.py` which allows + # the modified `.benchmarks/scripts/x_plus_42.py` to access `common` module`) + # Process each script under the base directory for path in scripts: # Read the script line by line From e8114cc47018cc23a8e532a228370fa1f8be60b1 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 7 Oct 2021 16:38:25 +0200 Subject: [PATCH 0383/1104] feat: add management of boolean binary operators with a const scalar refs #126 refs #529 --- concrete/numpy/tracing.py | 31 +++++------ .../common/optimization/test_float_fusing.py | 51 ++++++++++++++----- tests/numpy/test_tracing.py | 9 ++-- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 47321b6b5..d6e866bfe 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -249,9 +249,17 @@ class NPTracer(BaseTracer): # Supported functions are either univariate or bivariate for which one of the two # sources is a constant + # + # numpy.add, numpy.multiply and numpy.subtract are not there since already managed + # by leveled operations + # + # numpy.conjugate is not there since working on complex numbers + # + # numpy.isnat is not there since it is about timings + # + # numpy.divmod, numpy.modf and numpy.frexp are not there since output two values LIST_OF_SUPPORTED_UFUNC: List[numpy.ufunc] = [ numpy.absolute, - # numpy.add, numpy.arccos, numpy.arccosh, numpy.arcsin, @@ -264,14 +272,12 @@ class NPTracer(BaseTracer): numpy.bitwise_xor, numpy.cbrt, numpy.ceil, - # numpy.conjugate, numpy.copysign, numpy.cos, numpy.cosh, numpy.deg2rad, numpy.degrees, - # numpy.divmod, - # numpy.equal, + numpy.equal, numpy.exp, numpy.exp2, numpy.expm1, @@ -282,22 +288,20 @@ class NPTracer(BaseTracer): numpy.fmax, numpy.fmin, numpy.fmod, - # numpy.frexp, numpy.gcd, - # numpy.greater, - # numpy.greater_equal, + numpy.greater, + numpy.greater_equal, numpy.heaviside, numpy.hypot, - # numpy.invert, + numpy.invert, numpy.isfinite, numpy.isinf, numpy.isnan, - # numpy.isnat, numpy.lcm, numpy.ldexp, numpy.left_shift, - # numpy.less, - # numpy.less_equal, + numpy.less, + numpy.less_equal, numpy.log, numpy.log10, numpy.log1p, @@ -311,11 +315,9 @@ class NPTracer(BaseTracer): # numpy.matmul, numpy.maximum, numpy.minimum, - # numpy.modf, - # numpy.multiply, numpy.negative, numpy.nextafter, - # numpy.not_equal, + numpy.not_equal, numpy.positive, numpy.power, numpy.rad2deg, @@ -331,7 +333,6 @@ class NPTracer(BaseTracer): numpy.spacing, numpy.sqrt, numpy.square, - # numpy.subtract, numpy.tan, numpy.tanh, numpy.true_divide, diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index bbadfc9d3..a44234d2f 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -38,8 +38,10 @@ def simple_fuse_output(x): return x.astype(numpy.float64).astype(numpy.int32) -def complex_fuse_indirect_input(function, x, y): - """Complex fuse""" +def mix_x_and_y_intricately_and_call_f(function, x, y): + """Mix x and y in an intricated way, that can't be simplified by + an optimizer eg, and then call function + """ intermediate = x + y intermediate = intermediate + 2 intermediate = intermediate.astype(numpy.float32) @@ -57,8 +59,8 @@ def complex_fuse_indirect_input(function, x, y): ) -def complex_fuse_direct_input(function, x, y): - """Complex fuse""" +def mix_x_and_y_and_call_f(function, x, y): + """Mix x and y and then call function""" x_p_1 = x + 0.1 x_p_2 = x + 0.2 x_p_3 = function(x_p_1 + x_p_2) @@ -72,6 +74,21 @@ def complex_fuse_direct_input(function, x, y): ) +def mix_x_and_y_into_integer_and_call_f(function, x, y): + """Mix x and y but keep the entry to function as an integer""" + x_p_1 = x + 1 + x_p_2 = x + 2 + x_p_3 = function(x_p_1 + x_p_2) + return ( + x_p_3.astype(numpy.int32), + x_p_2.astype(numpy.int32), + (x_p_2 + 3).astype(numpy.int32), + x_p_3.astype(numpy.int32) + 67, + y, + (y + 4.7).astype(numpy.int32) + 3, + ) + + @pytest.mark.parametrize( "function_to_trace,fused", [ @@ -80,14 +97,14 @@ def complex_fuse_direct_input(function, x, y): pytest.param(simple_fuse_not_output, True, id="no_fuse"), pytest.param(simple_fuse_output, True, id="no_fuse"), pytest.param( - lambda x, y: complex_fuse_indirect_input(numpy.rint, x, y), + lambda x, y: mix_x_and_y_intricately_and_call_f(numpy.rint, x, y), True, - id="complex_fuse_indirect_input_with_rint", + id="mix_x_and_y_intricately_and_call_f_with_rint", ), pytest.param( - lambda x, y: complex_fuse_direct_input(numpy.rint, x, y), + lambda x, y: mix_x_and_y_and_call_f(numpy.rint, x, y), True, - id="complex_fuse_direct_input_with_rint", + id="mix_x_and_y_and_call_f_with_rint", ), ], ) @@ -152,13 +169,16 @@ def subtest_fuse_float_unary_operations_correctness(fun): # Some manipulation to avoid issues with domain of definitions of functions if fun == numpy.arccosh: input_list = [1, 2, 42, 44] - super_fun_list = [complex_fuse_direct_input] + super_fun_list = [mix_x_and_y_and_call_f] elif fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: input_list = [0, 0.1, 0.2] - super_fun_list = [complex_fuse_direct_input] + super_fun_list = [mix_x_and_y_and_call_f] + elif fun == numpy.invert: + input_list = [1, 2, 42, 44] + super_fun_list = [mix_x_and_y_into_integer_and_call_f] else: input_list = [0, 2, 42, 44] - super_fun_list = [complex_fuse_direct_input, complex_fuse_indirect_input] + super_fun_list = [mix_x_and_y_and_call_f, mix_x_and_y_intricately_and_call_f] for super_fun in super_fun_list: @@ -194,6 +214,7 @@ LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES = { numpy.bitwise_or, numpy.bitwise_xor, numpy.gcd, + numpy.invert, numpy.lcm, numpy.ldexp, numpy.left_shift, @@ -220,6 +241,10 @@ def subtest_fuse_float_binary_operations_correctness(fun): # a float output even for functions which return an integer (eg, XOR), such # that our frontend always try to fuse them + # The .astype(numpy.float64) that we have in cases 1 and 3 is here to force + # a float output even for functions which return a bool (eg, EQUAL), such + # that our frontend always try to fuse them + # For bivariate functions: fix one of the inputs if i == 0: # With an integer in first position @@ -229,7 +254,7 @@ def subtest_fuse_float_binary_operations_correctness(fun): elif i == 1: # With a float in first position def get_function_to_trace(): - return lambda x, y: fun(2.3, x + y).astype(numpy.int32) + return lambda x, y: fun(2.3, x + y).astype(numpy.float64).astype(numpy.int32) elif i == 2: # With an integer in second position @@ -239,7 +264,7 @@ def subtest_fuse_float_binary_operations_correctness(fun): else: # With a float in second position def get_function_to_trace(): - return lambda x, y: fun(x + y, 5.7).astype(numpy.int32) + return lambda x, y: fun(x + y, 5.7).astype(numpy.float64).astype(numpy.int32) input_list = [0, 2, 42, 44] diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 7b8b9472a..01b188878 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -62,6 +62,7 @@ LIST_OF_UFUNC_WHOSE_OUTPUT_IS_BOOL = set( numpy.isinf, numpy.isnan, numpy.signbit, + numpy.logical_not, ] ) @@ -406,15 +407,17 @@ def test_tracing_astype( ), ], ) -# numpy.logical_not is removed from the following test since it is expecting inputs which are -# integer only, as opposed to other functions in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC @pytest.mark.parametrize( "function_to_trace_def", - [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1 if f != numpy.logical_not], + [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1], ) def test_trace_numpy_supported_unary_ufuncs(inputs, expected_output_node, function_to_trace_def): """Function to trace supported numpy ufuncs""" + # numpy.invert is expecting inputs which are integer only + if function_to_trace_def == numpy.invert and not isinstance(inputs["x"].dtype, Integer): + return + # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint and flake8 are not happy # with it From 92ca061da995b0895e90089ff9474fe00cf81d41 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 7 Oct 2021 19:31:19 +0200 Subject: [PATCH 0384/1104] chore: fix release workflow - new tool to evaluate latest tag depends on some python packages - for now install the whole environment again - when poetry 1.2 is available use groups and only install required deps --- .github/workflows/continuous-integration.yaml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index c07109a89..aec95822f 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -462,6 +462,26 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + # To be removed once poetry 1.2 is released to manage dependencies with groups + - name: Cache Installation Files + uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 + with: + # Paths are Unix specific for now + path: | + ~/.cache/pip + ~/.cache/pypoetry + # Ignore line break in the evaluated double quoted string + key: "${{ runner.os }}-build-${{ matrix.python-version }}-\ + ${{ hashFiles('poetry.lock') }}" + restore-keys: | + ${{ runner.os }}-build-${{ matrix.python-version }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install poetry + make setup_env - name: Set tag in env run: | GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') @@ -476,7 +496,7 @@ jobs: # We want the space separated list of versions to be expanded # shellcheck disable=SC2086 - REQUIRES_LATEST_TAG=$(python script/make_utils/version_utils.py \ + REQUIRES_LATEST_TAG=$(poetry run python script/make_utils/version_utils.py \ islatest \ --new-version "${GIT_TAG}" \ --existing-versions $EXISTING_TAGS) From a7f00ec11109e49b9d7f8cef236a467ab82c736b Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 8 Oct 2021 17:27:32 +0300 Subject: [PATCH 0385/1104] fix(benchmarks): replace properties field of the machine information to specs --- .../progress_tracker_utils/extract_machine_info.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/script/progress_tracker_utils/extract_machine_info.py b/script/progress_tracker_utils/extract_machine_info.py index f65b6cc78..49cbc3682 100644 --- a/script/progress_tracker_utils/extract_machine_info.py +++ b/script/progress_tracker_utils/extract_machine_info.py @@ -13,22 +13,22 @@ def main(): """Extract some info about the host machine.""" dotenv.load_dotenv() - properties = [] + specs = [] cpu_value = cpuinfo.get_cpu_info()["brand_raw"].replace("(R)", "®").replace("(TM)", "™") - properties.append(["CPU", cpu_value]) + specs.append(["CPU", cpu_value]) vcpu_value = os.getenv("VCPU") if vcpu_value is not None: - properties.append(["vCPU", vcpu_value]) + specs.append(["vCPU", vcpu_value]) ram_value = f"{psutil.virtual_memory().total / (1024 ** 3):.2f} GB" - properties.append(["RAM", ram_value]) + specs.append(["RAM", ram_value]) os_value = os.getenv("OS_NAME") if os_value is None: os_value = f"{platform.system()} {platform.release()}" - properties.append(["OS", os_value]) + specs.append(["OS", os_value]) name = os.getenv("MACHINE_NAME") if name is None: @@ -47,7 +47,7 @@ def main(): os.makedirs(".benchmarks", exist_ok=True) - machine = {"id": id_, "name": name, "properties": properties} + machine = {"id": id_, "name": name, "specs": specs} with open(".benchmarks/machine.json", "w", encoding="utf-8") as f: json.dump(machine, f, indent=2, ensure_ascii=False) From 2cfb32d2c1afac7cca1fe75dc701315b41deef2d Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 8 Oct 2021 17:27:36 +0300 Subject: [PATCH 0386/1104] feat(benchmarks): add source code information to benchmarks --- script/progress_tracker_utils/measure.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 7c88c2cf3..e95529a26 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -308,7 +308,11 @@ def main(): raise RuntimeError(f"Target `{target_name}` is already registered") # Create an entry in the result for the current target - result["targets"][target_id] = {"name": target_name, "measurements": {}} + result["targets"][target_id] = { + "name": target_name, + "measurements": {}, + "code": "\n".join(lines), + } # Create a dictionary to hold `metric_id` to `metric_name` metrics = {} From fb9cc79128206b22f57cb3b00619d111f6f87e25 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 8 Oct 2021 14:46:13 +0200 Subject: [PATCH 0387/1104] chore: make sure xfail test cases are strict --- tests/common/data_types/test_dtypes_helpers.py | 4 ++-- tests/numpy/test_compile.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/common/data_types/test_dtypes_helpers.py b/tests/common/data_types/test_dtypes_helpers.py index d853d2b26..2ba821c8c 100644 --- a/tests/common/data_types/test_dtypes_helpers.py +++ b/tests/common/data_types/test_dtypes_helpers.py @@ -206,13 +206,13 @@ def test_mix_scalar_values(value1, value2, expected_mixed_value): ClearTensor(Integer(7, False), (1, 2, 3)), EncryptedScalar(Integer(7, False)), None, - marks=pytest.mark.xfail(raises=AssertionError), + marks=pytest.mark.xfail(strict=True, raises=AssertionError), ), pytest.param( ClearTensor(Integer(7, False), (1, 2, 3)), ClearTensor(Integer(7, False), (3, 2, 1)), None, - marks=pytest.mark.xfail(raises=AssertionError), + marks=pytest.mark.xfail(strict=True, raises=AssertionError), ), ], ) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 022b8cfb2..6e3d6347b 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -53,7 +53,7 @@ def small_fused_table(x): no_fuse_unhandled, ((-2, 2), (-2, 2)), ["x", "y"], - marks=pytest.mark.xfail(raises=ValueError), + marks=pytest.mark.xfail(strict=True, raises=ValueError), ), ], ) From 44016cc80c94658900e25fbdb06405c78e2df732 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 8 Oct 2021 09:59:56 +0200 Subject: [PATCH 0388/1104] feat: manage float fusing for tensors - add op_attributes on ArbitraryFunction - this works if all constants are smaller than the input tensor - otherwise it requires more advanced code and a concatenate operator which currently does not exist --- concrete/common/optimization/topological.py | 106 +++++++++++++--- .../common/representation/intermediate.py | 5 +- concrete/numpy/tracing.py | 10 ++ .../common/optimization/test_float_fusing.py | 118 ++++++++++++++---- 4 files changed, 196 insertions(+), 43 deletions(-) diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index 77bd5f947..e83af0ae1 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -1,13 +1,14 @@ """File holding topological optimization/simplification code.""" +import itertools from copy import deepcopy -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple, cast import networkx as nx from ..compilation.artifacts import CompilationArtifacts from ..data_types.floats import Float from ..data_types.integers import Integer -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true, custom_assert from ..operator_graph import OPGraph from ..representation.intermediate import ArbitraryFunction, Constant, Input, IntermediateNode from ..values import TensorValue @@ -39,10 +40,6 @@ def fuse_float_operations( float_subgraph_start_nodes, terminal_node, subgraph_all_nodes = float_subgraph_search_result processed_terminal_nodes.add(terminal_node) - # TODO: #199 To be removed when doing tensor management - if not subgraph_is_scalar_only(subgraph_all_nodes): - continue - subgraph_conversion_result = convert_float_subgraph_to_fused_node( op_graph, float_subgraph_start_nodes, @@ -111,16 +108,20 @@ def convert_float_subgraph_to_fused_node( output must be plugged as the input to the subgraph. """ - if not subgraph_has_unique_variable_input(float_subgraph_start_nodes): + subgraph_can_be_fused = subgraph_has_unique_variable_input( + float_subgraph_start_nodes + ) and subgraph_values_allow_fusing(float_subgraph_start_nodes, subgraph_all_nodes) + + if not subgraph_can_be_fused: return None # Only one variable input node, find which node feeds its input - non_constant_start_nodes = [ + variable_input_nodes = [ node for node in float_subgraph_start_nodes if not isinstance(node, Constant) ] - custom_assert(len(non_constant_start_nodes) == 1) + custom_assert(len(variable_input_nodes) == 1) - current_subgraph_variable_input = non_constant_start_nodes[0] + current_subgraph_variable_input = variable_input_nodes[0] new_input_value = deepcopy(current_subgraph_variable_input.outputs[0]) nx_graph = op_graph.graph @@ -244,20 +245,89 @@ def find_float_subgraph_with_unique_terminal_node( return float_subgraph_start_nodes, terminal_node, subgraph_all_nodes -# TODO: #199 To be removed when doing tensor management -def subgraph_is_scalar_only(subgraph_all_nodes: Set[IntermediateNode]) -> bool: - """Check subgraph only processes scalars. +def subgraph_values_allow_fusing( + float_subgraph_start_nodes: Set[IntermediateNode], + subgraph_all_nodes: Set[IntermediateNode], +): + """Check if a subgraph's values are compatible with fusing. + + A fused subgraph for example only works on an input tensor if the resulting ArbitraryFunction + can be applied per cell, hence shuffling or tensor shape changes make fusing impossible. Args: - subgraph_all_nodes (Set[IntermediateNode]): The nodes of the float subgraph. + float_subgraph_start_nodes (Set[IntermediateNode]): The nodes starting the float subgraph. + subgraph_all_nodes (Set[IntermediateNode]): All the nodes in the float subgraph. Returns: - bool: True if all inputs and outputs of the nodes in the subgraph are scalars. + bool: True if all inputs and outputs of the nodes in the subgraph are compatible with fusing + i.e. outputs have the same shapes equal to the variable input. """ - return all( - all(isinstance(input_, TensorValue) and input_.is_scalar for input_ in node.inputs) - and all(isinstance(output, TensorValue) and output.is_scalar for output in node.outputs) + + variable_input_nodes = [ + node for node in float_subgraph_start_nodes if not isinstance(node, Constant) + ] + + assert_true( + (num_variable_input_nodes := len(variable_input_nodes)) == 1, + f"{subgraph_values_allow_fusing.__name__} " + f"only works for subgraphs with 1 variable input node, got {num_variable_input_nodes}", + ) + + # Some ArbitraryFunction nodes have baked constants that need to be taken into account for the + # max size computation + baked_constants_ir_nodes = [ + baked_constant_base_value for node in subgraph_all_nodes + if isinstance(node, ArbitraryFunction) + if (baked_constant_base_value := node.op_attributes.get("baked_constant_ir_node", None)) + is not None + ] + + all_values_are_tensors = all( + all(isinstance(input_, TensorValue) for input_ in node.inputs) + and all(isinstance(output, TensorValue) for output in node.outputs) + for node in itertools.chain(subgraph_all_nodes, baked_constants_ir_nodes) + ) + + if not all_values_are_tensors: + # This cannot be reached today as scalars are Tensors with shape == () (numpy convention) + return False # pragma: no cover + + variable_input_node = variable_input_nodes[0] + + # A cheap check is that the variable input node must have the biggest size, i.e. have the most + # elements, meaning all constants will broadcast to its shape. This is because the + # ArbitraryFunction input and output must have the same shape so that it can be applied to each + # of the input tensor cells. + # There *may* be a way to manage the other case by simulating the broadcast of the smaller input + # array and then concatenating/stacking the results. This is not currently doable as we don't + # have a concatenate operator on the compiler side. + # TODO: #587 https://github.com/zama-ai/concretefhe-internal/issues/587 + + variable_input_node_output = cast(TensorValue, variable_input_node.outputs[0]) + variable_input_node_output_size, variable_input_node_output_shape = ( + variable_input_node_output.size, + variable_input_node_output.shape, + ) + max_inputs_size = max( + cast(TensorValue, input_node.outputs[0]).size + for input_node in itertools.chain(subgraph_all_nodes, baked_constants_ir_nodes) + ) + + if variable_input_node_output_size < max_inputs_size: + return False + + # Now that we know the variable input node has the biggest size we can check shapes are + # consistent throughout the subgraph: outputs of ir nodes that are not constant must be equal. + + non_constant_nodes = (node for node in subgraph_all_nodes if not isinstance(node, Constant)) + + return all( + all( + isinstance(output, TensorValue) and output.shape == variable_input_node_output_shape + for output in node.outputs + ) + for node in non_constant_nodes ) diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index da66a20d5..e5d5328ab 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -202,9 +202,10 @@ class ArbitraryFunction(IntermediateNode): # The arbitrary_func is not optional but mypy has a long standing bug and is not able to # understand this properly. See https://github.com/python/mypy/issues/708#issuecomment-605636623 arbitrary_func: Optional[Callable] + op_name: str op_args: Tuple[Any, ...] op_kwargs: Dict[str, Any] - op_name: str + op_attributes: Dict[str, Any] _n_in: int = 1 def __init__( @@ -215,12 +216,14 @@ class ArbitraryFunction(IntermediateNode): op_name: Optional[str] = None, op_args: Optional[Tuple[Any, ...]] = None, op_kwargs: Optional[Dict[str, Any]] = None, + op_attributes: Optional[Dict[str, Any]] = None, ) -> None: super().__init__([input_base_value]) custom_assert(len(self.inputs) == 1) self.arbitrary_func = arbitrary_func self.op_args = op_args if op_args is not None else () self.op_kwargs = op_kwargs if op_kwargs is not None else {} + self.op_attributes = op_attributes if op_attributes is not None else {} output = deepcopy(input_base_value) output.dtype = output_dtype diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index d6e866bfe..1c62ace0a 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -208,6 +208,15 @@ class NPTracer(BaseTracer): op_kwargs = deepcopy(kwargs) op_kwargs["baked_constant"] = baked_constant + # Store info on the operation being treated + # Currently: the base value and type corresponding to the baked constant and which input idx + # it was feeding + op_attributes = { + "baked_constant_ir_node": deepcopy( + input_tracers[in_which_input_is_constant].traced_computation + ), + "in_which_input_is_constant": in_which_input_is_constant, + } traced_computation = ArbitraryFunction( input_base_value=input_tracers[in_which_input_is_variable].output, @@ -215,6 +224,7 @@ class NPTracer(BaseTracer): output_dtype=common_output_dtypes[0], op_kwargs=op_kwargs, op_name=binary_operator_string, + op_attributes=op_attributes, ) output_tracer = cls( (input_tracers[in_which_input_is_variable],), diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index a44234d2f..40826ddeb 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -7,6 +7,7 @@ import numpy import pytest from concrete.common.data_types.integers import Integer +from concrete.common.debugging.custom_assert import assert_not_reached from concrete.common.optimization.topological import fuse_float_operations from concrete.common.values import EncryptedScalar, EncryptedTensor from concrete.numpy import tracing @@ -134,17 +135,27 @@ def test_fuse_float_operations(function_to_trace, fused, input_): assert function_to_trace(*inputs) == op_graph(*inputs) -# TODO: #199 To be removed when doing tensor management -def test_tensor_no_fuse(): +def subtest_tensor_no_fuse(fun, tensor_shape): """Test case to verify float fusing is only applied on functions on scalars.""" - ndim = random.randint(1, 3) - tensor_shape = tuple(random.randint(1, 10) for _ in range(ndim + 1)) + if tensor_shape == (): + # We want tensors + return + + if fun in LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES: + # We need at least one input of the bivariate function to be float + return + + # Float fusing currently cannot work if the constant in a bivariate operator is bigger than the + # variable input. + # Make a broadcastable shape but with the constant being bigger + variable_tensor_shape = (1,) + tensor_shape + constant_bigger_shape = (random.randint(2, 10),) + tensor_shape def tensor_no_fuse(x): intermediate = x.astype(numpy.float64) - intermediate = intermediate.astype(numpy.int32) - return intermediate + numpy.ones(tensor_shape) + intermediate = fun(intermediate, numpy.ones(constant_bigger_shape)) + return intermediate.astype(numpy.int32) function_to_trace = tensor_no_fuse params_names = signature(function_to_trace).parameters.keys() @@ -152,7 +163,7 @@ def test_tensor_no_fuse(): op_graph = trace_numpy_function( function_to_trace, { - param_name: EncryptedTensor(Integer(32, True), shape=tensor_shape) + param_name: EncryptedTensor(Integer(32, True), shape=variable_tensor_shape) for param_name in params_names }, ) @@ -163,7 +174,24 @@ def test_tensor_no_fuse(): assert orig_num_nodes == fused_num_nodes -def subtest_fuse_float_unary_operations_correctness(fun): +def check_results_are_equal(function_result, op_graph_result): + """Check the output of function execution and OPGraph evaluation are equal.""" + + if isinstance(function_result, tuple) and isinstance(op_graph_result, tuple): + assert len(function_result) == len(op_graph_result) + are_equal = ( + function_output == op_graph_output + for function_output, op_graph_output in zip(function_result, op_graph_result) + ) + elif not isinstance(function_result, tuple) and not isinstance(op_graph_result, tuple): + are_equal = (function_result == op_graph_result,) + else: + assert_not_reached(f"Incompatible outputs: {function_result}, {op_graph_result}") + + return all(value.all() if isinstance(value, numpy.ndarray) else value for value in are_equal) + + +def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): """Test a unary function with fuse_float_operations.""" # Some manipulation to avoid issues with domain of definitions of functions @@ -193,7 +221,10 @@ def subtest_fuse_float_unary_operations_correctness(fun): op_graph = trace_numpy_function( function_to_trace, - {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + { + param_name: EncryptedTensor(Integer(32, True), tensor_shape) + for param_name in params_names + }, ) orig_num_nodes = len(op_graph.graph) fuse_float_operations(op_graph) @@ -201,12 +232,20 @@ def subtest_fuse_float_unary_operations_correctness(fun): assert fused_num_nodes < orig_num_nodes - input_ = numpy.int32(input_) + ones_input = ( + numpy.ones(tensor_shape, dtype=numpy.dtype(type(input_))) + if tensor_shape != () + else 1 + ) + input_ = numpy.int32(input_ * ones_input) num_params = len(params_names) inputs = (input_,) * num_params - assert function_to_trace(*inputs) == op_graph(*inputs) + function_result = function_to_trace(*inputs) + op_graph_result = op_graph(*inputs) + + assert check_results_are_equal(function_result, op_graph_result) LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES = { @@ -227,7 +266,7 @@ LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES = { } -def subtest_fuse_float_binary_operations_correctness(fun): +def subtest_fuse_float_binary_operations_correctness(fun, tensor_shape): """Test a binary functions with fuse_float_operations, with a constant as a source.""" for i in range(4): @@ -248,23 +287,37 @@ def subtest_fuse_float_binary_operations_correctness(fun): # For bivariate functions: fix one of the inputs if i == 0: # With an integer in first position + ones_0 = numpy.ones(tensor_shape, dtype=numpy.int64) if tensor_shape != () else 1 + def get_function_to_trace(): - return lambda x, y: fun(3, x + y).astype(numpy.float64).astype(numpy.int32) + return lambda x, y: fun(3 * ones_0, x + y).astype(numpy.float64).astype(numpy.int32) elif i == 1: # With a float in first position + ones_1 = numpy.ones(tensor_shape, dtype=numpy.float64) if tensor_shape != () else 1 + def get_function_to_trace(): - return lambda x, y: fun(2.3, x + y).astype(numpy.float64).astype(numpy.int32) + return ( + lambda x, y: fun(2.3 * ones_1, x + y).astype(numpy.float64).astype(numpy.int32) + ) elif i == 2: # With an integer in second position + ones_2 = numpy.ones(tensor_shape, dtype=numpy.int64) if tensor_shape != () else 1 + def get_function_to_trace(): - return lambda x, y: fun(x + y, 4).astype(numpy.float64).astype(numpy.int32) + return lambda x, y: fun(x + y, 4 * ones_2).astype(numpy.float64).astype(numpy.int32) else: # With a float in second position + ones_else = numpy.ones(tensor_shape, dtype=numpy.float64) if tensor_shape != () else 1 + def get_function_to_trace(): - return lambda x, y: fun(x + y, 5.7).astype(numpy.float64).astype(numpy.int32) + return ( + lambda x, y: fun(x + y, 5.7 * ones_else) + .astype(numpy.float64) + .astype(numpy.int32) + ) input_list = [0, 2, 42, 44] @@ -273,6 +326,12 @@ def subtest_fuse_float_binary_operations_correctness(fun): input_list = [2, 42, 44] for input_ in input_list: + ones_input = ( + numpy.ones(tensor_shape, dtype=numpy.dtype(type(input_))) + if tensor_shape != () + else 1 + ) + input_ = input_ * ones_input function_to_trace = get_function_to_trace() @@ -280,7 +339,10 @@ def subtest_fuse_float_binary_operations_correctness(fun): op_graph = trace_numpy_function( function_to_trace, - {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + { + param_name: EncryptedTensor(Integer(32, True), tensor_shape) + for param_name in params_names + }, ) orig_num_nodes = len(op_graph.graph) fuse_float_operations(op_graph) @@ -293,10 +355,13 @@ def subtest_fuse_float_binary_operations_correctness(fun): num_params = len(params_names) inputs = (input_,) * num_params - assert function_to_trace(*inputs) == op_graph(*inputs) + function_result = function_to_trace(*inputs) + op_graph_result = op_graph(*inputs) + + assert check_results_are_equal(function_result, op_graph_result) -def subtest_fuse_float_binary_operations_dont_support_two_variables(fun): +def subtest_fuse_float_binary_operations_dont_support_two_variables(fun, tensor_shape): """Test a binary function with fuse_float_operations, with no constant as a source.""" @@ -310,18 +375,23 @@ def subtest_fuse_float_binary_operations_dont_support_two_variables(fun): with pytest.raises(NotImplementedError, match=r"Can't manage binary operator"): trace_numpy_function( function_to_trace, - {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + { + param_name: EncryptedTensor(Integer(32, True), tensor_shape) + for param_name in params_names + }, ) @pytest.mark.parametrize("fun", tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC) -def test_ufunc_operations(fun): +@pytest.mark.parametrize("tensor_shape", [(), (3, 1, 2)]) +def test_ufunc_operations(fun, tensor_shape): """Test functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" if fun.nin == 1: - subtest_fuse_float_unary_operations_correctness(fun) + subtest_fuse_float_unary_operations_correctness(fun, tensor_shape) elif fun.nin == 2: - subtest_fuse_float_binary_operations_correctness(fun) - subtest_fuse_float_binary_operations_dont_support_two_variables(fun) + subtest_fuse_float_binary_operations_correctness(fun, tensor_shape) + subtest_fuse_float_binary_operations_dont_support_two_variables(fun, tensor_shape) + subtest_tensor_no_fuse(fun, tensor_shape) else: raise NotImplementedError("Only unary and binary functions are tested for now") From 00916bcfdbc91e91af649acf2e4e6f00fa572ad9 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 11 Oct 2021 09:53:03 +0200 Subject: [PATCH 0389/1104] refactor: rename ArbitraryFunction to UnivariateFunction - the naming has always been confusing and recent changes to the code make this rename necessary for things to be clearer --- concrete/common/debugging/drawing.py | 8 +++--- concrete/common/debugging/printing.py | 4 +-- concrete/common/extensions/table.py | 6 ++-- concrete/common/mlir/converters.py | 6 ++-- concrete/common/mlir/utils.py | 6 ++-- concrete/common/optimization/topological.py | 22 +++++++-------- .../common/representation/intermediate.py | 13 +++++---- concrete/numpy/compile.py | 2 +- concrete/numpy/tracing.py | 8 +++--- docs/dev/explanation/FLOAT-FUSING.md | 4 +-- docs/user/tutorial/COMPILATION_ARTIFACTS.md | 2 +- tests/common/extensions/test_table.py | 12 ++++---- .../representation/test_intermediate.py | 28 +++++++++---------- tests/conftest.py | 10 +++---- tests/numpy/test_tracing.py | 10 +++---- 15 files changed, 72 insertions(+), 69 deletions(-) diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index c06115996..65ce67628 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -14,12 +14,12 @@ from ..operator_graph import OPGraph from ..representation.intermediate import ( ALL_IR_NODES, Add, - ArbitraryFunction, Constant, Dot, Input, Mul, Sub, + UnivariateFunction, ) IR_NODE_COLOR_MAPPING = { @@ -28,9 +28,9 @@ IR_NODE_COLOR_MAPPING = { Add: "red", Sub: "yellow", Mul: "green", - ArbitraryFunction: "orange", + UnivariateFunction: "orange", Dot: "purple", - "ArbitraryFunction": "orange", + "UnivariateFunction": "orange", "TLU": "grey", "output": "magenta", } @@ -71,7 +71,7 @@ def draw_graph( value_to_return = IR_NODE_COLOR_MAPPING[type(node)] if node in output_nodes: value_to_return = IR_NODE_COLOR_MAPPING["output"] - elif isinstance(node, ArbitraryFunction): + elif isinstance(node, UnivariateFunction): value_to_return = IR_NODE_COLOR_MAPPING.get(node.op_name, value_to_return) return value_to_return diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 0bc093b70..8187139ce 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -6,7 +6,7 @@ import networkx as nx from ..debugging.custom_assert import custom_assert from ..operator_graph import OPGraph -from ..representation.intermediate import ArbitraryFunction, Constant, Input +from ..representation.intermediate import Constant, Input, UnivariateFunction def output_data_type_to_string(node): @@ -61,7 +61,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: base_name = node.__class__.__name__ - if isinstance(node, ArbitraryFunction): + if isinstance(node, UnivariateFunction): base_name = node.op_name what_to_print = base_name + "(" diff --git a/concrete/common/extensions/table.py b/concrete/common/extensions/table.py index 971a4309f..8e882bd52 100644 --- a/concrete/common/extensions/table.py +++ b/concrete/common/extensions/table.py @@ -6,7 +6,7 @@ from typing import Iterable, Tuple, Union from ..common_helpers import is_a_power_of_2 from ..data_types.base import BaseDataType from ..data_types.integers import make_integer_to_hold -from ..representation.intermediate import ArbitraryFunction +from ..representation.intermediate import UnivariateFunction from ..tracing.base_tracer import BaseTracer @@ -32,10 +32,10 @@ class LookupTable: def __getitem__(self, key: Union[int, BaseTracer]): # if a tracer is used for indexing, - # we need to create an `ArbitraryFunction` node + # we need to create an `UnivariateFunction` node # because the result will be determined during the runtime if isinstance(key, BaseTracer): - traced_computation = ArbitraryFunction( + traced_computation = UnivariateFunction( input_base_value=key.output, arbitrary_func=LookupTable._checked_indexing, output_dtype=self.output_dtype, diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index aae80ad1b..255a6720a 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -22,7 +22,7 @@ from ..data_types.dtypes_helpers import ( ) from ..data_types.integers import Integer from ..debugging.custom_assert import custom_assert -from ..representation.intermediate import Add, ArbitraryFunction, Constant, Dot, Mul, Sub +from ..representation.intermediate import Add, Constant, Dot, Mul, Sub, UnivariateFunction from ..values import TensorValue @@ -165,7 +165,7 @@ def constant(node, _, __, ctx): def apply_lut(node, preds, ir_to_mlir_node, ctx): - """Convert an arbitrary function intermediate node.""" + """Convert a UnivariateFunction intermediate node.""" custom_assert(len(node.inputs) == 1, "LUT should have a single input") custom_assert(len(node.outputs) == 1, "LUT should have a single output") if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): @@ -224,7 +224,7 @@ V0_OPSET_CONVERSION_FUNCTIONS = { Sub: sub, Mul: mul, Constant: constant, - ArbitraryFunction: apply_lut, + UnivariateFunction: apply_lut, Dot: dot, } diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 28280a4ad..6c37cddf1 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -11,7 +11,7 @@ from ..data_types.dtypes_helpers import ( ) from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph -from ..representation.intermediate import ArbitraryFunction +from ..representation.intermediate import UnivariateFunction # TODO: should come from compiler, through an API, #402 ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB = 7 @@ -81,7 +81,7 @@ def update_bit_width_for_mlir(op_graph: OPGraph): # TODO: remove this workaround, which was for #279, once the compiler can handle # smaller tables, #412 - has_a_table = any(isinstance(node, ArbitraryFunction) for node in op_graph.graph.nodes) + has_a_table = any(isinstance(node, UnivariateFunction) for node in op_graph.graph.nodes) if has_a_table: max_bit_width = ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB @@ -104,7 +104,7 @@ def extend_direct_lookup_tables(op_graph: OPGraph): op_graph: graph to update lookup tables for """ for node in op_graph.graph.nodes: - if isinstance(node, ArbitraryFunction) and node.op_name == "TLU": + if isinstance(node, UnivariateFunction) and node.op_name == "TLU": table = node.op_kwargs["table"] bit_width = cast(Integer, node.inputs[0].dtype).bit_width expected_length = 2 ** bit_width diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index e83af0ae1..b9941cc10 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -10,7 +10,7 @@ from ..data_types.floats import Float from ..data_types.integers import Integer from ..debugging.custom_assert import assert_true, custom_assert from ..operator_graph import OPGraph -from ..representation.intermediate import ArbitraryFunction, Constant, Input, IntermediateNode +from ..representation.intermediate import Constant, Input, IntermediateNode, UnivariateFunction from ..values import TensorValue @@ -18,7 +18,7 @@ def fuse_float_operations( op_graph: OPGraph, compilation_artifacts: Optional[CompilationArtifacts] = None, ): - """Find and fuse float domains into single Integer to Integer ArbitraryFunction. + """Find and fuse float domains into single Integer to Integer UnivariateFunction. Args: op_graph (OPGraph): The OPGraph to simplify @@ -92,8 +92,8 @@ def convert_float_subgraph_to_fused_node( float_subgraph_start_nodes: Set[IntermediateNode], terminal_node: IntermediateNode, subgraph_all_nodes: Set[IntermediateNode], -) -> Optional[Tuple[ArbitraryFunction, IntermediateNode]]: - """Convert a float subgraph to an equivalent fused ArbitraryFunction node. +) -> Optional[Tuple[UnivariateFunction, IntermediateNode]]: + """Convert a float subgraph to an equivalent fused UnivariateFunction node. Args: op_graph (OPGraph): The OPGraph the float subgraph is part of. @@ -103,7 +103,7 @@ def convert_float_subgraph_to_fused_node( subgraph_all_nodes (Set[IntermediateNode]): All the nodes in the float subgraph. Returns: - Optional[Tuple[ArbitraryFunction, IntermediateNode]]: None if the float subgraph + Optional[Tuple[UnivariateFunction, IntermediateNode]]: None if the float subgraph cannot be fused, otherwise returns a tuple containing the fused node and the node whose output must be plugged as the input to the subgraph. """ @@ -161,7 +161,7 @@ def convert_float_subgraph_to_fused_node( ) # Create fused_node - fused_node = ArbitraryFunction( + fused_node = UnivariateFunction( deepcopy(new_subgraph_variable_input.inputs[0]), lambda x, float_op_subgraph, terminal_node: float_op_subgraph.evaluate({0: x})[ terminal_node @@ -251,7 +251,7 @@ def subgraph_values_allow_fusing( ): """Check if a subgraph's values are compatible with fusing. - A fused subgraph for example only works on an input tensor if the resulting ArbitraryFunction + A fused subgraph for example only works on an input tensor if the resulting UnivariateFunction can be applied per cell, hence shuffling or tensor shape changes make fusing impossible. Args: @@ -273,12 +273,12 @@ def subgraph_values_allow_fusing( f"only works for subgraphs with 1 variable input node, got {num_variable_input_nodes}", ) - # Some ArbitraryFunction nodes have baked constants that need to be taken into account for the + # Some UnivariateFunction nodes have baked constants that need to be taken into account for the # max size computation baked_constants_ir_nodes = [ baked_constant_base_value for node in subgraph_all_nodes - if isinstance(node, ArbitraryFunction) + if isinstance(node, UnivariateFunction) if (baked_constant_base_value := node.op_attributes.get("baked_constant_ir_node", None)) is not None ] @@ -297,7 +297,7 @@ def subgraph_values_allow_fusing( # A cheap check is that the variable input node must have the biggest size, i.e. have the most # elements, meaning all constants will broadcast to its shape. This is because the - # ArbitraryFunction input and output must have the same shape so that it can be applied to each + # UnivariateFunction input and output must have the same shape so that it can be applied to each # of the input tensor cells. # There *may* be a way to manage the other case by simulating the broadcast of the smaller input # array and then concatenating/stacking the results. This is not currently doable as we don't @@ -343,5 +343,5 @@ def subgraph_has_unique_variable_input( bool: True if only one of the nodes is not an Constant """ # Only one input to the subgraph where computations are done in floats is variable, this - # is the only case we can manage with ArbitraryFunction fusing + # is the only case we can manage with UnivariateFunction fusing return sum(not isinstance(node, Constant) for node in float_subgraph_start_nodes) == 1 diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index e5d5328ab..2183a8597 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -196,11 +196,14 @@ class Constant(IntermediateNode): return str(self.constant_data) -class ArbitraryFunction(IntermediateNode): - """Node representing a univariate arbitrary function, e.g. sin(x).""" +class UnivariateFunction(IntermediateNode): + """Node representing an univariate arbitrary function, e.g. sin(x).""" # The arbitrary_func is not optional but mypy has a long standing bug and is not able to # understand this properly. See https://github.com/python/mypy/issues/708#issuecomment-605636623 + # arbitrary_func can take more than one argument but during evaluation the input variable will + # be the first argument passed to it. You can add other constant arguments needed for the proper + # execution of the function through op_args and op_kwargs. arbitrary_func: Optional[Callable] op_name: str op_args: Tuple[Any, ...] @@ -240,9 +243,9 @@ class ArbitraryFunction(IntermediateNode): return self.op_name def get_table(self) -> List[Any]: - """Get the table for the current input value of this ArbitraryFunction. + """Get the table for the current input value of this UnivariateFunction. - This function only works if the ArbitraryFunction input value is an unsigned Integer. + This function only works if the UnivariateFunction input value is an unsigned Integer. Returns: List[Any]: The table. @@ -290,7 +293,7 @@ class Dot(IntermediateNode): """Return the node representing a dot product.""" _n_in: int = 2 - # Optional, same issue as in ArbitraryFunction for mypy + # Optional, same issue as in UnivariateFunction for mypy evaluation_function: Optional[Callable[[Any, Any], Any]] # Allows to use specialized implementations from e.g. numpy diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index bf8419715..689b9c5b6 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -96,7 +96,7 @@ def _compile_numpy_function_into_op_graph_internal( # Apply topological optimizations if they are enabled if compilation_configuration.enable_topological_optimizations: - # Fuse float operations to have int to int ArbitraryFunction + # Fuse float operations to have int to int UnivariateFunction if not check_op_graph_is_integer_program(op_graph): fuse_float_operations(op_graph, compilation_artifacts) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 1c62ace0a..c9b5c6966 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -9,7 +9,7 @@ from numpy.typing import DTypeLike from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype from ..common.debugging.custom_assert import assert_true, custom_assert from ..common.operator_graph import OPGraph -from ..common.representation.intermediate import ArbitraryFunction, Constant, Dot +from ..common.representation.intermediate import Constant, Dot, UnivariateFunction from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from ..common.values import BaseValue from .np_dtypes_helpers import ( @@ -87,7 +87,7 @@ class NPTracer(BaseTracer): normalized_numpy_dtype = numpy.dtype(numpy_dtype) output_dtype = convert_numpy_dtype_to_base_data_type(numpy_dtype) - traced_computation = ArbitraryFunction( + traced_computation = UnivariateFunction( input_base_value=self.output, arbitrary_func=normalized_numpy_dtype.type, output_dtype=output_dtype, @@ -154,7 +154,7 @@ class NPTracer(BaseTracer): common_output_dtypes = cls._manage_dtypes(unary_operator, *input_tracers) custom_assert(len(common_output_dtypes) == 1) - traced_computation = ArbitraryFunction( + traced_computation = UnivariateFunction( input_base_value=input_tracers[0].output, arbitrary_func=unary_operator, output_dtype=common_output_dtypes[0], @@ -218,7 +218,7 @@ class NPTracer(BaseTracer): "in_which_input_is_constant": in_which_input_is_constant, } - traced_computation = ArbitraryFunction( + traced_computation = UnivariateFunction( input_base_value=input_tracers[in_which_input_is_variable].output, arbitrary_func=arbitrary_func, output_dtype=common_output_dtypes[0], diff --git a/docs/dev/explanation/FLOAT-FUSING.md b/docs/dev/explanation/FLOAT-FUSING.md index ac10088d8..9a20ace5f 100644 --- a/docs/dev/explanation/FLOAT-FUSING.md +++ b/docs/dev/explanation/FLOAT-FUSING.md @@ -31,7 +31,7 @@ The float subgraph that was detected: ![](../../_static/float_fusing_example/subgraph.png) -The simplified graph of operations with the float subgraph condensed in an `ArbitraryFunction` node: +The simplified graph of operations with the float subgraph condensed in an `UnivariateFunction` node: ![](../../_static/float_fusing_example/after.png) @@ -39,7 +39,7 @@ The simplified graph of operations with the float subgraph condensed in an `Arbi The first step consists in detecting where we go from floating point computation back to integers. This allows to identify the potential terminal node of the float subgraph we are going to fuse. -From the terminal node, we go back up through the nodes until we find nodes that go from integers to floats. If we can guarantee the identified float subgraph has a single variable integer input then we can replace it by an equivalent ArbitraryFunction node. +From the terminal node, we go back up through the nodes until we find nodes that go from integers to floats. If we can guarantee the identified float subgraph has a single variable integer input then we can replace it by an equivalent UnivariateFunction node. An example of a non fusable computation with that technique is: diff --git a/docs/user/tutorial/COMPILATION_ARTIFACTS.md b/docs/user/tutorial/COMPILATION_ARTIFACTS.md index 8000d63d5..6c3a72100 100644 --- a/docs/user/tutorial/COMPILATION_ARTIFACTS.md +++ b/docs/user/tutorial/COMPILATION_ARTIFACTS.md @@ -83,7 +83,7 @@ Traceback (most recent call last): File "/src/concrete/numpy/compile.py", line 103, in _compile_numpy_function_into_op_graph_internal raise ValueError( ValueError: cannot be compiled as it has nodes with either float inputs or outputs. -Offending nodes : +Offending nodes : ``` ## Manual export diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index cdd0481a2..d6a95999a 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -55,8 +55,8 @@ def test_lookup_table_encrypted_lookup(test_helpers): ref_graph.add_node(input_x) # pylint: disable=protected-access - # Need access to _checked_indexing to have is_equivalent_to work for ir.ArbitraryFunction - output_arbitrary_function = ir.ArbitraryFunction( + # Need access to _checked_indexing to have is_equivalent_to work for ir.UnivariateFunction + output_arbitrary_function = ir.UnivariateFunction( input_base_value=x, arbitrary_func=LookupTable._checked_indexing, output_dtype=table.output_dtype, @@ -68,7 +68,7 @@ def test_lookup_table_encrypted_lookup(test_helpers): ref_graph.add_edge(input_x, output_arbitrary_function, input_idx=0) - # TODO: discuss if this check is enough as == is not overloaded properly for ArbitraryFunction + # TODO: discuss if this check is enough as == is not overloaded properly for UnivariateFunction assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) @@ -95,8 +95,8 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): ref_graph.add_node(input_x) # pylint: disable=protected-access - # Need access to _checked_indexing to have is_equivalent_to work for ir.ArbitraryFunction - intermediate_arbitrary_function = ir.ArbitraryFunction( + # Need access to _checked_indexing to have is_equivalent_to work for ir.UnivariateFunction + intermediate_arbitrary_function = ir.UnivariateFunction( input_base_value=x, arbitrary_func=LookupTable._checked_indexing, output_dtype=table.output_dtype, @@ -117,5 +117,5 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): ref_graph.add_edge(intermediate_arbitrary_function, output_add, input_idx=0) ref_graph.add_edge(constant_3, output_add, input_idx=1) - # TODO: discuss if this check is enough as == is not overloaded properly for ArbitraryFunction + # TODO: discuss if this check is enough as == is not overloaded properly for UnivariateFunction assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 34251437e..847a0e562 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -34,15 +34,15 @@ from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, En pytest.param(ir.Constant(42), None, 42, id="Constant"), pytest.param(ir.Constant(-42), None, -42, id="Constant"), pytest.param( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(7, False)), lambda x: x + 3, Integer(7, False) ), [10], 13, - id="ArbitraryFunction, x + 3", + id="UnivariateFunction, x + 3", ), pytest.param( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(7, False)), lambda x, y: x + y, Integer(7, False), @@ -50,10 +50,10 @@ from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, En ), [10], 13, - id="ArbitraryFunction, (x, y) -> x + y, where y is constant == 3", + id="UnivariateFunction, (x, y) -> x + y, where y is constant == 3", ), pytest.param( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(7, False)), lambda x, y: y[x], Integer(7, False), @@ -61,10 +61,10 @@ from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, En ), [2], 3, - id="ArbitraryFunction, (x, y) -> y[x], where y is constant == (1, 2, 3, 4)", + id="UnivariateFunction, (x, y) -> y[x], where y is constant == (1, 2, 3, 4)", ), pytest.param( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(7, False)), lambda x, y: y[3], Integer(7, False), @@ -72,7 +72,7 @@ from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, En ), [2], 4, - id="ArbitraryFunction, x, y -> y[3], where y is constant == (1, 2, 3, 4)", + id="UnivariateFunction, x, y -> y[3], where y is constant == (1, 2, 3, 4)", ), pytest.param( ir.Dot( @@ -209,34 +209,34 @@ def test_evaluate( False, ), ( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) ), - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) ), True, ), ( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False), op_args=(1, 2, 3), ), - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) ), False, ), ( - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False), op_kwargs={"tuple": (1, 2, 3)}, ), - ir.ArbitraryFunction( + ir.UnivariateFunction( EncryptedScalar(Integer(8, False)), lambda x: x, Integer(8, False) ), False, diff --git a/tests/conftest.py b/tests/conftest.py index 2a0d48873..c5d45b98b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,13 +9,13 @@ import pytest from concrete.common.representation.intermediate import ( ALL_IR_NODES, Add, - ArbitraryFunction, Constant, Dot, Input, IntermediateNode, Mul, Sub, + UnivariateFunction, ) @@ -66,10 +66,10 @@ def python_functions_are_equal_or_equivalent(lhs: object, rhs: object) -> bool: return False -def is_equivalent_arbitrary_function(lhs: ArbitraryFunction, rhs: object) -> bool: - """Helper function to check if an ArbitraryFunction node is equivalent to an other object.""" +def is_equivalent_arbitrary_function(lhs: UnivariateFunction, rhs: object) -> bool: + """Helper function to check if an UnivariateFunction node is equivalent to an other object.""" return ( - isinstance(rhs, ArbitraryFunction) + isinstance(rhs, UnivariateFunction) and python_functions_are_equal_or_equivalent(lhs.arbitrary_func, rhs.arbitrary_func) and lhs.op_args == rhs.op_args and lhs.op_kwargs == rhs.op_kwargs @@ -127,7 +127,7 @@ def is_equivalent_intermediate_node(lhs: IntermediateNode, rhs: object) -> bool: EQUIVALENT_TEST_FUNC: Dict[Type, Callable[..., bool]] = { Add: is_equivalent_add, - ArbitraryFunction: is_equivalent_arbitrary_function, + UnivariateFunction: is_equivalent_arbitrary_function, Constant: is_equivalent_constant, Dot: is_equivalent_dot, Input: is_equivalent_input, diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 01b188878..95e500c34 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -386,24 +386,24 @@ def test_tracing_astype( [ pytest.param( {"x": EncryptedScalar(Integer(7, is_signed=False))}, - ir.ArbitraryFunction, + ir.UnivariateFunction, ), pytest.param( {"x": EncryptedScalar(Integer(32, is_signed=True))}, - ir.ArbitraryFunction, + ir.UnivariateFunction, ), pytest.param( {"x": EncryptedScalar(Integer(64, is_signed=True))}, - ir.ArbitraryFunction, + ir.UnivariateFunction, ), pytest.param( {"x": EncryptedScalar(Integer(128, is_signed=True))}, - ir.ArbitraryFunction, + ir.UnivariateFunction, marks=pytest.mark.xfail(strict=True, raises=NotImplementedError), ), pytest.param( {"x": EncryptedScalar(Float(64))}, - ir.ArbitraryFunction, + ir.UnivariateFunction, ), ], ) From e8b8869ae89bf69ec33745f37c0d0707eb88b84b Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 11 Oct 2021 14:02:09 +0200 Subject: [PATCH 0390/1104] feat: write %2 = Add(%0, %1) instead of %2 = Add(0, 1) refs #601 --- concrete/common/debugging/printing.py | 2 +- tests/numpy/test_compile.py | 2 +- tests/numpy/test_debugging.py | 78 +++++++++++++-------------- tests/numpy/test_tracing.py | 24 ++++----- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 8187139ce..fc367a2c1 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -82,7 +82,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: custom_assert([x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name)))) # Then, just print the predecessors in the right order - what_to_print += ", ".join([x[1] for x in list_of_arg_name]) + ")" + what_to_print += ", ".join(["%" + x[1] for x in list_of_arg_name]) + ")" # This code doesn't work with more than a single output new_line = f"%{i} = {what_to_print}" diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 6e3d6347b..f0c5aa948 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -364,7 +364,7 @@ def test_small_inputset_treat_warnings_as_errors(): "# EncryptedTensor, shape=(4,)>" "\n%1 = y " "# EncryptedTensor, shape=(4,)>" - "\n%2 = Dot(0, 1) " + "\n%2 = Dot(%0, %1) " "# EncryptedScalar>" "\nreturn(%2)\n", ), diff --git a/tests/numpy/test_debugging.py b/tests/numpy/test_debugging.py index a7342c485..b19ab4aff 100644 --- a/tests/numpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -40,23 +40,23 @@ def issue_130_c(x, y): @pytest.mark.parametrize( "lambda_f,ref_graph_str", [ - (lambda x, y: x + y, "%0 = x\n%1 = y\n%2 = Add(0, 1)\nreturn(%2)\n"), - (lambda x, y: x - y, "%0 = x\n%1 = y\n%2 = Sub(0, 1)\nreturn(%2)\n"), - (lambda x, y: x + x, "%0 = x\n%1 = Add(0, 0)\nreturn(%1)\n"), + (lambda x, y: x + y, "%0 = x\n%1 = y\n%2 = Add(%0, %1)\nreturn(%2)\n"), + (lambda x, y: x - y, "%0 = x\n%1 = y\n%2 = Sub(%0, %1)\nreturn(%2)\n"), + (lambda x, y: x + x, "%0 = x\n%1 = Add(%0, %0)\nreturn(%1)\n"), ( lambda x, y: x + x - y * y * y + x, - "%0 = x\n%1 = y\n%2 = Add(0, 0)\n%3 = Mul(1, 1)" - "\n%4 = Mul(3, 1)\n%5 = Sub(2, 4)\n%6 = Add(5, 0)\nreturn(%6)\n", + "%0 = x\n%1 = y\n%2 = Add(%0, %0)\n%3 = Mul(%1, %1)" + "\n%4 = Mul(%3, %1)\n%5 = Sub(%2, %4)\n%6 = Add(%5, %0)\nreturn(%6)\n", ), - (lambda x, y: x + 1, "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)\n"), - (lambda x, y: 1 + x, "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2)\n"), - (lambda x, y: (-1) + x, "%0 = x\n%1 = Constant(-1)\n%2 = Add(0, 1)\nreturn(%2)\n"), - (lambda x, y: 3 * x, "%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)\n"), - (lambda x, y: x * 3, "%0 = x\n%1 = Constant(3)\n%2 = Mul(0, 1)\nreturn(%2)\n"), - (lambda x, y: x * (-3), "%0 = x\n%1 = Constant(-3)\n%2 = Mul(0, 1)\nreturn(%2)\n"), - (lambda x, y: x - 11, "%0 = x\n%1 = Constant(11)\n%2 = Sub(0, 1)\nreturn(%2)\n"), - (lambda x, y: 11 - x, "%0 = Constant(11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)\n"), - (lambda x, y: (-11) - x, "%0 = Constant(-11)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2)\n"), + (lambda x, y: x + 1, "%0 = x\n%1 = Constant(1)\n%2 = Add(%0, %1)\nreturn(%2)\n"), + (lambda x, y: 1 + x, "%0 = x\n%1 = Constant(1)\n%2 = Add(%0, %1)\nreturn(%2)\n"), + (lambda x, y: (-1) + x, "%0 = x\n%1 = Constant(-1)\n%2 = Add(%0, %1)\nreturn(%2)\n"), + (lambda x, y: 3 * x, "%0 = x\n%1 = Constant(3)\n%2 = Mul(%0, %1)\nreturn(%2)\n"), + (lambda x, y: x * 3, "%0 = x\n%1 = Constant(3)\n%2 = Mul(%0, %1)\nreturn(%2)\n"), + (lambda x, y: x * (-3), "%0 = x\n%1 = Constant(-3)\n%2 = Mul(%0, %1)\nreturn(%2)\n"), + (lambda x, y: x - 11, "%0 = x\n%1 = Constant(11)\n%2 = Sub(%0, %1)\nreturn(%2)\n"), + (lambda x, y: 11 - x, "%0 = Constant(11)\n%1 = x\n%2 = Sub(%0, %1)\nreturn(%2)\n"), + (lambda x, y: (-11) - x, "%0 = Constant(-11)\n%1 = x\n%2 = Sub(%0, %1)\nreturn(%2)\n"), ( lambda x, y: x + 13 - y * (-21) * y + 44, "%0 = Constant(44)" @@ -64,11 +64,11 @@ def issue_130_c(x, y): "\n%2 = Constant(13)" "\n%3 = y" "\n%4 = Constant(-21)" - "\n%5 = Add(1, 2)" - "\n%6 = Mul(3, 4)" - "\n%7 = Mul(6, 3)" - "\n%8 = Sub(5, 7)" - "\n%9 = Add(8, 0)" + "\n%5 = Add(%1, %2)" + "\n%6 = Mul(%3, %4)" + "\n%7 = Mul(%6, %3)" + "\n%8 = Sub(%5, %7)" + "\n%9 = Add(%8, %0)" "\nreturn(%9)\n", ), # Multiple outputs @@ -78,9 +78,9 @@ def issue_130_c(x, y): "\n%1 = Constant(1)" "\n%2 = Constant(2)" "\n%3 = y" - "\n%4 = Add(0, 1)" - "\n%5 = Add(0, 3)" - "\n%6 = Add(5, 2)" + "\n%4 = Add(%0, %1)" + "\n%5 = Add(%0, %3)" + "\n%6 = Add(%5, %2)" "\nreturn(%4, %6)\n", ), ( @@ -89,28 +89,28 @@ def issue_130_c(x, y): ), ( lambda x, y: (x, x + 1), - "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%0, %2)\n", + "%0 = x\n%1 = Constant(1)\n%2 = Add(%0, %1)\nreturn(%0, %2)\n", ), ( lambda x, y: (x + 1, x + 1), "%0 = x" "\n%1 = Constant(1)" "\n%2 = Constant(1)" - "\n%3 = Add(0, 1)" - "\n%4 = Add(0, 2)" + "\n%3 = Add(%0, %1)" + "\n%4 = Add(%0, %2)" "\nreturn(%3, %4)\n", ), ( issue_130_a, - "%0 = x\n%1 = Constant(1)\n%2 = Add(0, 1)\nreturn(%2, %2)\n", + "%0 = x\n%1 = Constant(1)\n%2 = Add(%0, %1)\nreturn(%2, %2)\n", ), ( issue_130_b, - "%0 = x\n%1 = Constant(1)\n%2 = Sub(0, 1)\nreturn(%2, %2)\n", + "%0 = x\n%1 = Constant(1)\n%2 = Sub(%0, %1)\nreturn(%2, %2)\n", ), ( issue_130_c, - "%0 = Constant(1)\n%1 = x\n%2 = Sub(0, 1)\nreturn(%2, %2)\n", + "%0 = Constant(1)\n%1 = x\n%2 = Sub(%0, %1)\nreturn(%2, %2)\n", ), ], ) @@ -155,12 +155,12 @@ def test_print_and_draw_graph(lambda_f, ref_graph_str, x_y): ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x\n%1 = TLU(0)\nreturn(%1)\n", + "%0 = x\n%1 = TLU(%0)\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x\n%1 = Constant(4)\n%2 = Add(0, 1)\n%3 = TLU(2)\nreturn(%3)\n", + "%0 = x\n%1 = Constant(4)\n%2 = Add(%0, %1)\n%3 = TLU(%2)\nreturn(%3)\n", ), ], ) @@ -189,7 +189,7 @@ def test_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph_str): "x": EncryptedTensor(Integer(2, is_signed=False), shape=(3,)), "y": EncryptedTensor(Integer(2, is_signed=False), shape=(3,)), }, - "%0 = x\n%1 = y\n%2 = Dot(0, 1)\nreturn(%2)\n", + "%0 = x\n%1 = y\n%2 = Dot(%0, %1)\nreturn(%2)\n", ), # pylint: enable=unnecessary-lambda ], @@ -223,7 +223,7 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): ), "%0 = x # EncryptedScalar>" "\n%1 = y # EncryptedScalar>" - "\n%2 = Add(0, 1) # EncryptedScalar>" + "\n%2 = Add(%0, %1) # EncryptedScalar>" "\nreturn(%2)\n", ), ( @@ -236,7 +236,7 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): "# EncryptedScalar>" "\n%1 = y " "# EncryptedScalar>" - "\n%2 = Mul(0, 1) " + "\n%2 = Mul(%0, %1) " "# EncryptedScalar>" "\nreturn(%2)\n", ), @@ -264,7 +264,7 @@ def test_print_with_show_data_types(lambda_f, x_y, ref_graph_str): {"x": EncryptedScalar(Integer(2, is_signed=False))}, "%0 = x " "# EncryptedScalar>" - "\n%1 = TLU(0) " + "\n%1 = TLU(%0) " "# EncryptedScalar>" "\nreturn(%1)\n", ), @@ -275,9 +275,9 @@ def test_print_with_show_data_types(lambda_f, x_y, ref_graph_str): "# EncryptedScalar>" "\n%1 = Constant(4) " "# ClearScalar>" - "\n%2 = Add(0, 1) " + "\n%2 = Add(%0, %1) " "# EncryptedScalar>" - "\n%3 = TLU(2) " + "\n%3 = TLU(%2) " "# EncryptedScalar>" "\nreturn(%3)\n", ), @@ -288,11 +288,11 @@ def test_print_with_show_data_types(lambda_f, x_y, ref_graph_str): "# EncryptedScalar>" "\n%1 = Constant(4) " "# ClearScalar>" - "\n%2 = Add(0, 1) " + "\n%2 = Add(%0, %1) " "# EncryptedScalar>" - "\n%3 = TLU(2) " + "\n%3 = TLU(%2) " "# EncryptedScalar>" - "\n%4 = TLU(3) " + "\n%4 = TLU(%3) " "# EncryptedScalar>" "\nreturn(%4)\n", ), diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 95e500c34..e07f73fb9 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -195,12 +195,12 @@ def test_numpy_tracing_tensors(): %4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> %5 = x # EncryptedTensor, shape=(2, 2)> %6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> -%7 = Add(5, 6) # EncryptedTensor, shape=(2, 2)> -%8 = Add(4, 7) # EncryptedTensor, shape=(2, 2)> -%9 = Sub(3, 8) # EncryptedTensor, shape=(2, 2)> -%10 = Sub(9, 2) # EncryptedTensor, shape=(2, 2)> -%11 = Mul(10, 1) # EncryptedTensor, shape=(2, 2)> -%12 = Mul(0, 11) # EncryptedTensor, shape=(2, 2)> +%7 = Add(%5, %6) # EncryptedTensor, shape=(2, 2)> +%8 = Add(%4, %7) # EncryptedTensor, shape=(2, 2)> +%9 = Sub(%3, %8) # EncryptedTensor, shape=(2, 2)> +%10 = Sub(%9, %2) # EncryptedTensor, shape=(2, 2)> +%11 = Mul(%10, %1) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(%0, %11) # EncryptedTensor, shape=(2, 2)> return(%12) """.lstrip() @@ -234,12 +234,12 @@ def test_numpy_explicit_tracing_tensors(): %4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> %5 = x # EncryptedTensor, shape=(2, 2)> %6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> -%7 = Add(5, 6) # EncryptedTensor, shape=(2, 2)> -%8 = Add(4, 7) # EncryptedTensor, shape=(2, 2)> -%9 = Sub(3, 8) # EncryptedTensor, shape=(2, 2)> -%10 = Sub(9, 2) # EncryptedTensor, shape=(2, 2)> -%11 = Mul(10, 1) # EncryptedTensor, shape=(2, 2)> -%12 = Mul(0, 11) # EncryptedTensor, shape=(2, 2)> +%7 = Add(%5, %6) # EncryptedTensor, shape=(2, 2)> +%8 = Add(%4, %7) # EncryptedTensor, shape=(2, 2)> +%9 = Sub(%3, %8) # EncryptedTensor, shape=(2, 2)> +%10 = Sub(%9, %2) # EncryptedTensor, shape=(2, 2)> +%11 = Mul(%10, %1) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(%0, %11) # EncryptedTensor, shape=(2, 2)> return(%12) """.lstrip() From de3a9f9bb394c17a5f2b5b10361ca484bc7c8aea Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 11 Oct 2021 14:09:56 +0200 Subject: [PATCH 0391/1104] chore: update release workflow for auto GitHub release creation - update version_utils.py script to get info about prerelease - update release template --- .github/ISSUE_TEMPLATE/release.md | 9 +-- .github/workflows/continuous-integration.yaml | 71 ++++++++++++++++--- script/make_utils/version_utils.py | 9 ++- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index ba66bc187..bf5241ed8 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -21,14 +21,7 @@ VERSION=X.Y.Z-rc? make set_version Then: - [ ] For non RC releases: check the release milestone issues, cut out what can't be completed in time and change the milestones for these issues - [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Z-rc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Z-rc?`) -- [ ] Wait for the release workflow to finish and get the image url from the notification or the logs -- [ ] See [here](https://docs.github.com/en/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release) to create the release in GitHub using the existing tag, add the pull link copied from the step before \(`ghcr.io/zama-ai/concretefhe:vX.Y.Z`\) (or `vX.Y.Z-rc?`) for the uploaded docker image. Mark release as pre-release for an `rc` version. See template below: - -This is the release markdown template you should copy and update: -``` -**Docker Image:** ghcr.io/zama-ai/concretefhe:vX.Y.Z -**Documentation:** https://docs.zama.ai/concrete/ -``` +- [ ] Wait for the release workflow to finish and check everything went well. To continue the release cycle: - [ ] Choose the version number for next release, e.g. `vA.B.C` (can be `vA.B.C-rc?` for Release Candidates) following semantic versioning: https://semver.org/ diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index aec95822f..210a5a7d3 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -154,7 +154,9 @@ jobs: shell: '/usr/bin/bash -e {0}' strategy: matrix: - python-version: [3.8] + # YAML footgun : https://twitter.com/webology/status/1445394072492023811?s=20 + # Versions need to be quoted or risk being interpreted as floating point numbers + python-version: ["3.8"] outputs: report: ${{ steps.report.outputs.report || 'Did not run.' }} @@ -470,11 +472,10 @@ jobs: path: | ~/.cache/pip ~/.cache/pypoetry - # Ignore line break in the evaluated double quoted string - key: "${{ runner.os }}-build-${{ matrix.python-version }}-\ - ${{ hashFiles('poetry.lock') }}" + # Use python 3.8 as it is the version available in ubuntu 20.04 and we develop with it + key: "${{ runner.os }}-build-3.8-${{ hashFiles('poetry.lock') }}" restore-keys: | - ${{ runner.os }}-build-${{ matrix.python-version }}- + ${{ runner.os }}-build-3.8- ${{ runner.os }}-build- ${{ runner.os }}- - name: Install dependencies @@ -485,6 +486,7 @@ jobs: - name: Set tag in env run: | GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') + echo "GIT_TAG=${GIT_TAG}" >> "$GITHUB_ENV" RELEASE_IMG_GIT_TAG="${RELEASE_IMAGE_BASE}:${GIT_TAG}" echo "RELEASE_IMG_GIT_TAG=${RELEASE_IMG_GIT_TAG}" >> "$GITHUB_ENV" RELEASE_IMG_TAGS_TO_PUSH="${RELEASE_IMG_GIT_TAG}" @@ -496,11 +498,15 @@ jobs: # We want the space separated list of versions to be expanded # shellcheck disable=SC2086 - REQUIRES_LATEST_TAG=$(poetry run python script/make_utils/version_utils.py \ + IS_LATEST_INFO=$(poetry run python script/make_utils/version_utils.py \ islatest \ --new-version "${GIT_TAG}" \ --existing-versions $EXISTING_TAGS) + REQUIRES_LATEST_TAG=$(echo "${IS_LATEST_INFO}" | jq -rc '.is_latest') + IS_PRERELEASE=$(echo "${IS_LATEST_INFO}" | jq -rc '.is_prerelease') + echo "IS_PRERELEASE=${IS_PRERELEASE}" >> "$GITHUB_ENV" + if [[ "${REQUIRES_LATEST_TAG}" == "true" ]]; then RELEASE_IMG_LATEST_TAG="${RELEASE_IMAGE_BASE}:latest" RELEASE_IMG_TAGS_TO_PUSH="${RELEASE_IMG_TAGS_TO_PUSH},${RELEASE_IMG_LATEST_TAG}" @@ -534,12 +540,61 @@ jobs: docker run --rm -v "$(pwd)"/docker/release_resources:/data \ "${RELEASE_IMG_GIT_TAG}" /bin/bash -c "python ./sanity_check.py" docker image push --all-tags "${RELEASE_IMAGE_BASE}" + - name: Create directory for artifacts + if: ${{ success() && !cancelled() }} + run: | + ARTIFACTS_RAW_DIR=/tmp/release_artifacts/raw/ + mkdir -p "${ARTIFACTS_RAW_DIR}" + echo "ARTIFACTS_RAW_DIR=${ARTIFACTS_RAW_DIR}" >> "$GITHUB_ENV" + + ARTIFACTS_PACKAGED_DIR=/tmp/release_artifacts/packaged/ + mkdir -p "${ARTIFACTS_PACKAGED_DIR}" + echo "ARTIFACTS_PACKAGED_DIR=${ARTIFACTS_PACKAGED_DIR}" >> "$GITHUB_ENV" + - name: Download Documentation + if: ${{ success() && !cancelled() }} + id: download-docs + uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60 + with: + name: html-docs + path: ${{ env.ARTIFACTS_RAW_DIR }}/html_docs/ + - name: Download changelog + if: ${{ success() && !cancelled() }} + id: download-changelog + uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60 + with: + name: changelog + path: ${{ env.ARTIFACTS_RAW_DIR }}/changelog/ + - name: Create ready to upload/packaged artifacts + if: ${{ success() && !cancelled() }} + env: + RAW_DOCS_DIR: ${{ steps.download-docs.outputs.download-path }} + RAW_CHANGELOG_DIR: ${{ steps.download-changelog.outputs.download-path }} + run: | + pushd "${RAW_DOCS_DIR}" + zip -r "${ARTIFACTS_PACKAGED_DIR}/html-docs.zip" ./* + tar -cvzf "${ARTIFACTS_PACKAGED_DIR}/html-docs.tar.gz" ./* + popd + cp "${RAW_CHANGELOG_DIR}/*" "${ARTIFACTS_PACKAGED_DIR}" + - name: Create GitHub release + if: ${{ success() && !cancelled() }} + id: create-release + uses: softprops/action-gh-release@6034af24fba4e5a8e975aaa6056554efe4c794d0 + with: + body: | + **Docker Image:** ${{ env.RELEASE_IMG_GIT_TAG }} + **Documentation:** https://docs.zama.ai/concrete/ + prerelease: ${{ fromJSON(env.IS_PRERELEASE) }} + files: | + '${{ env.ARTIFACTS_PACKAGED_DIR }}/*' + tag_name: ${{ env.GIT_TAG }} + fail_on_unmatched_files: true + token: ${{ secrets.BOT_TOKEN }} - name: Set notification report id: report if: ${{ always() }} run: | - REPORT="Pushing docker image ${{ env.RELEASE_IMG_TAGS_TO_PUSH }} finished with status \ - ${{ job.status }}." + REPORT="Creating release for ${GIT_TAG} finished with status ${{ job.status }}. \ + GitHub release link: ${{ steps.create-release.outputs.url }}." echo "${REPORT}" echo "::set-output name=report::${REPORT}" echo "REPORT=${REPORT}" >> "$GITHUB_ENV" diff --git a/script/make_utils/version_utils.py b/script/make_utils/version_utils.py index 58150f36f..f8a58ac38 100644 --- a/script/make_utils/version_utils.py +++ b/script/make_utils/version_utils.py @@ -1,6 +1,7 @@ """Tool to manage version in the project""" import argparse +import json import os import re import sys @@ -20,7 +21,8 @@ def islatest(args): """islatest command entry point.""" print(args, file=sys.stderr) - new_version_is_latest = False + # This is the safest default + result = {"is_latest": False, "is_prerelease": True} new_version_str = strip_leading_v(args.new_version) if VersionInfo.isvalid(new_version_str): @@ -43,7 +45,10 @@ def islatest(args): all_non_prerelease_version_infos.append(new_version_info) new_version_is_latest = max(all_non_prerelease_version_infos) == new_version_info - print(str(new_version_is_latest).lower()) + result["is_latest"] = new_version_is_latest + result["is_prerelease"] = False + + print(json.dumps(result)) def update_variable_in_py_file(file_path: Path, var_name: str, version_str: str): From 8490f88227aff85aacf5edd87f65fded75f7ba65 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 11 Oct 2021 16:58:24 +0200 Subject: [PATCH 0392/1104] chore: change coverage to have global infos - have a small hack to dump pytest-cov report --- .github/workflows/continuous-integration.yaml | 2 +- .gitignore | 1 + Makefile | 3 +- script/actions_utils/coverage.sh | 19 +---- .../actions_utils/coverage_report_format.py | 74 +++++++++++++------ tests/conftest.py | 30 ++++++++ 6 files changed, 88 insertions(+), 41 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 210a5a7d3..3526e6cfe 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -287,7 +287,7 @@ jobs: id: coverage if: ${{ always() && steps.pytest.outcome != 'skipped' && !cancelled() }} run: | - ./script/actions_utils/coverage.sh ${{ github.base_ref }} + ./script/actions_utils/coverage.sh global-coverage-infos.json - name: Archive test coverage uses: actions/upload-artifact@27121b0bdffd731efa15d66772be8dc71245d074 if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} diff --git a/.gitignore b/.gitignore index 679f9b1ff..5e34e4312 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ diff-coverage.txt *.py,cover .hypothesis/ .pytest_cache/ +global-coverage-infos.json # Translations *.mo diff --git a/Makefile b/Makefile index 12b7b600b..8b9719eda 100644 --- a/Makefile +++ b/Makefile @@ -78,8 +78,9 @@ pcc_internal: $(PCC_DEPS) pytest: poetry run pytest -svv \ + --global-coverage-infos-json global-coverage-infos.json \ --cov=$(SRC_DIR) --cov-fail-under=100 \ - --cov-report=term-missing:skip-covered --cov-report=xml tests/ + --cov-report=term-missing:skip-covered tests/ .PHONY: pytest # Not a huge fan of ignoring missing imports, but some packages do not have typing stubs diff --git a/script/actions_utils/coverage.sh b/script/actions_utils/coverage.sh index 90f400e2f..e8ecc73c3 100755 --- a/script/actions_utils/coverage.sh +++ b/script/actions_utils/coverage.sh @@ -5,21 +5,8 @@ set +e CURR_DIR=$(dirname "$0") -# Run diff-coverage -if [[ "$1" == "" ]]; then - export BB="origin/main" -else - export BB="origin/$1" -fi -make coverage | tee diff-coverage.txt - -# Get exit code without closing the script -TEST_EXIT_CODE="$?" - # Format diff-coverage.txt for PR comment poetry run python "$CURR_DIR"/coverage_report_format.py \ ---diff-cover-exit-code "$TEST_EXIT_CODE" \ ---diff-cover-output diff-coverage.txt - -# Set exit code to the diff coverage check -exit "$TEST_EXIT_CODE" +global-coverage \ +--global-coverage-json-file "$1" \ +--global-coverage-output-file diff-coverage.txt diff --git a/script/actions_utils/coverage_report_format.py b/script/actions_utils/coverage_report_format.py index b27f0149f..bfc4d09f6 100755 --- a/script/actions_utils/coverage_report_format.py +++ b/script/actions_utils/coverage_report_format.py @@ -2,21 +2,14 @@ """Helper script for github actions""" import argparse -import traceback +import json from pathlib import Path -def main(args): - """Entry point""" - diff_cover_file_path = Path(args.diff_cover_output).resolve().absolute() - - diff_cover_content = None - - with open(diff_cover_file_path, "r", encoding="utf-8") as f: - diff_cover_content = f.readlines() - - with open(diff_cover_file_path, "w", encoding="utf-8") as f: - if args.diff_cover_exit_code == 0: +def write_coverage_file(coverage_file_path: Path, exit_code: int, coverage_content): + """Write the formatted coverage to file.""" + with open(coverage_file_path, "w", encoding="utf-8") as f: + if exit_code == 0: f.write("## Coverage passed ✅\n\n") else: f.write("## Coverage failed ❌\n\n") @@ -25,24 +18,59 @@ def main(args): f.write("
Coverage details\n

\n\n") f.write("```\n") - f.writelines(diff_cover_content) + f.writelines(coverage_content) # Close collapsible section f.write("```\n\n") f.write("

\n
\n\n") +def diff_coverage(args): + """diff-coverage entry point.""" + diff_cover_file_path = Path(args.diff_cover_output).resolve() + diff_cover_content = None + + with open(diff_cover_file_path, "r", encoding="utf-8") as f: + diff_cover_content = f.readlines() + + write_coverage_file(diff_cover_file_path, args.diff_cover_exit_code, diff_cover_content) + + +def global_coverage(args): + """global-coverage entry point.""" + global_coverage_json_path = Path(args.global_coverage_json_file).resolve() + global_coverage_infos = None + with open(global_coverage_json_path, "r", encoding="utf-8") as f: + global_coverage_infos = json.load(f) + + exit_code = global_coverage_infos["exit_code"] + coverage_content = global_coverage_infos["content"] + global_coverage_output_file_path = Path(args.global_coverage_output_file).resolve() + write_coverage_file(global_coverage_output_file_path, exit_code, coverage_content) + + +def main(args): + """Entry point""" + args.entry_point(args) + + if __name__ == "__main__": - parser = argparse.ArgumentParser(allow_abbrev=False) + main_parser = argparse.ArgumentParser(allow_abbrev=False) - parser.add_argument("--diff-cover-exit-code", type=int, required=True) - parser.add_argument("--diff-cover-output", type=str, required=True) + sub_parsers = main_parser.add_subparsers(dest="sub-command", required=True) - cli_args = parser.parse_args() + parser_diff_coverage = sub_parsers.add_parser("diff-coverage") - # pylint: disable=broad-except - try: - main(cli_args) - except Exception: - traceback.print_exc() - # pylint: enable=broad-except + parser_diff_coverage.add_argument("--diff-cover-exit-code", type=int, required=True) + parser_diff_coverage.add_argument("--diff-cover-output", type=str, required=True) + parser_diff_coverage.set_defaults(entry_point=diff_coverage) + + parser_global_coverage = sub_parsers.add_parser("global-coverage") + + parser_global_coverage.add_argument("--global-coverage-output-file", type=str, required=True) + parser_global_coverage.add_argument("--global-coverage-json-file", type=str, required=True) + parser_global_coverage.set_defaults(entry_point=global_coverage) + + cli_args = main_parser.parse_args() + + main(cli_args) diff --git a/tests/conftest.py b/tests/conftest.py index c5d45b98b..5b41347e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ """PyTest configuration file""" +import json import operator +from pathlib import Path from typing import Callable, Dict, Type import networkx as nx @@ -19,6 +21,34 @@ from concrete.common.representation.intermediate import ( ) +def pytest_addoption(parser): + """Options for pytest""" + + parser.addoption( + "--global-coverage-infos-json", + type=str, + help="To dump pytest-cov term report to a text file.", + ) + + +def pytest_sessionfinish(session: pytest.Session, exitstatus): + """Pytest callback when testing ends.""" + # Hacked together from the source code, they don't have an option to export to file and it's too + # much work to get a PR in for such a little thing + # https://github.com/pytest-dev/pytest-cov/blob/ + # ec344d8adf2d78238d8f07cb20ed2463d7536970/src/pytest_cov/plugin.py#L329 + if session.config.pluginmanager.hasplugin("_cov"): + global_coverage_file = session.config.getoption( + "--global-coverage-infos-json", default=None + ) + if global_coverage_file is not None: + cov_plugin = session.config.pluginmanager.getplugin("_cov") + coverage_txt = cov_plugin.cov_report.getvalue() + global_coverage_file_path = Path(global_coverage_file).resolve() + with open(global_coverage_file_path, "w", encoding="utf-8") as f: + json.dump({"exit_code": exitstatus, "content": coverage_txt}, f) + + def _is_equivalent_to_binary_commutative(lhs: IntermediateNode, rhs: object) -> bool: """is_equivalent_to for a binary and commutative operation.""" return ( From 3c85d6360362a7d5b5abb8428275280afabe44cc Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 11 Oct 2021 17:06:37 +0200 Subject: [PATCH 0393/1104] chore: fix release workflow dependencies install - graphviz is required for the whole project, we don't have dependencies groups yet (waiting for poetry 1.2) so we need to install it --- .github/workflows/continuous-integration.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 3526e6cfe..2b5829040 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -478,8 +478,11 @@ jobs: ${{ runner.os }}-build-3.8- ${{ runner.os }}-build- ${{ runner.os }}- + # See #570 To be updated to only install required dependencies group with poetry 1.2 and + # remove graphviz installs which are only required for the actual package and not dev tools - name: Install dependencies run: | + sudo apt-get install --no-install-recommends -y graphviz* python -m pip install --upgrade pip python -m pip install poetry make setup_env From ea7e117815be47eba6dae42c0afbbc3e2200b5cd Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 11 Oct 2021 16:10:23 +0200 Subject: [PATCH 0394/1104] chore: let's try to check optional scopes refs #583 --- .github/workflows/continuous-integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 2b5829040..755f327b3 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -195,10 +195,10 @@ jobs: if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: - pattern: '^((feat|fix|chore|refactor|style|test|docs)(\(\w+\))?\:) .+$' + pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values)\))?\:) .+$' flags: 'gs' error: "Your first line has to contain a commit type and scope like \"feat(my_feature): msg\".\ - Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\(\\w+\\))?\\:)'" + Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values)\\))?\\:)'" excludeDescription: 'true' # optional: this excludes the description body of a pull request excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request From fd40a8b95184b9574165045c54f0103e6bdae547 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 10:13:05 +0200 Subject: [PATCH 0395/1104] chore: fix release workflow - globs don't expand in quoted strings to the contrary of variables --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 755f327b3..c432b7a63 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -577,7 +577,7 @@ jobs: zip -r "${ARTIFACTS_PACKAGED_DIR}/html-docs.zip" ./* tar -cvzf "${ARTIFACTS_PACKAGED_DIR}/html-docs.tar.gz" ./* popd - cp "${RAW_CHANGELOG_DIR}/*" "${ARTIFACTS_PACKAGED_DIR}" + cp "${RAW_CHANGELOG_DIR}"/* "${ARTIFACTS_PACKAGED_DIR}" - name: Create GitHub release if: ${{ success() && !cancelled() }} id: create-release From 1dbc961dbb03abf1318753907ef280b3ee451c2a Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 10:41:35 +0200 Subject: [PATCH 0396/1104] fix: disable pip version warning during environment scan - made some tests fail if pip is not the latest version --- concrete/common/compilation/artifacts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index b1b48b90d..474a4b9b8 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -146,7 +146,9 @@ class CompilationArtifacts: # wrapt 1.12.1 # zipp 3.5.0 - pip_process = subprocess.run(["pip", "list"], stdout=subprocess.PIPE, check=True) + pip_process = subprocess.run( + ["pip", "--disable-pip-version-check", "list"], stdout=subprocess.PIPE, check=True + ) dependencies = iter(pip_process.stdout.decode("utf-8").split("\n")) # skip 'Package ... Version' line From 53efda71cf5980b648d9625c244c528e5acc7530 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Mon, 11 Oct 2021 16:16:20 +0200 Subject: [PATCH 0397/1104] chore: update doc theme to v0.4.1 --- docs/_static/css/zama.css | 31 ++++++++++++++--- docs/_static/favicon.ico | Bin 0 -> 33310 bytes docs/{logo-black.png => _static/logo.png} | Bin docs/_templates/layout.html | 40 ++++++++++++---------- docs/conf.py | 10 ++++-- 5 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 docs/_static/favicon.ico rename docs/{logo-black.png => _static/logo.png} (100%) diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index bd74c1373..a80d41c2b 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -11,8 +11,8 @@ --secondary-color-light: #fff0d9; --tertiary-color: #414042; --tertiary-color-light: #e6e7e8; - --link-color: var(--primary-color-darker); - --primary-font: Telegraf, sans-serif; + --link-color: black; + --primary-font: Archivo, sans-serif; } .zama-header { @@ -21,7 +21,6 @@ right: 0; z-index: 201; height: 43px; - max-width: 1100px; background: black; color: white; display: flex; @@ -54,6 +53,11 @@ body { font-family: var(--primary-font); } +/* Change code blocks font size slightly (normally 12px)*/ +.rst-content pre.literal-block, .rst-content div[class^="highlight"] pre, .rst-content .linenodiv pre { + font-size: 13px !important; +} + .rst-content .toctree-wrapper>p.caption, h1, h2, h3, h4, h5, h6, legend { font-family: var(--primary-font); } @@ -120,6 +124,10 @@ input[type=color], input[type=date], input[type=datetime-local], input[type=date .wy-nav-content a { color: var(--link-color); + text-decoration: underline; +} + +.rst-footer-buttons a { text-decoration: none; } @@ -127,7 +135,7 @@ input[type=color], input[type=date], input[type=datetime-local], input[type=date .wy-nav-content a:focus, .wy-nav-content a:active { color: var(--link-color); - text-decoration: underline; + text-decoration: none; } .wy-nav-content a.btn:hover, @@ -137,6 +145,19 @@ input[type=color], input[type=date], input[type=datetime-local], input[type=date text-decoration: none; } +.wy-nav-content { + max-width: none; + background-color: white; +} + +.rst-content { + max-width: 800px; + display: block; + margin-left: auto; + margin-right: auto; +} + + .wy-nav-top { color: #696969; background: #343131 @@ -191,6 +212,8 @@ dl.class > dt:first-of-type { display: block !important; } + + /* * Admonitions */ diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fb2fa5a1562916b4a49f74913fbcca2abd43e652 GIT binary patch literal 33310 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 literal 0 HcmV?d00001 diff --git a/docs/logo-black.png b/docs/_static/logo.png similarity index 100% rename from docs/logo-black.png rename to docs/_static/logo.png diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 5883f91f4..8c127267f 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -16,6 +16,24 @@ + + {#- Do not conflict with RTD insertion of analytics script #} + {%- if not READTHEDOCS %} + {%- if theme_analytics_id %} + + + + {%- endif %} + {%- endif %} + {{- metatags }} @@ -125,7 +143,9 @@
- Docs home + {%- if show_docs_home_link %} + Docs home + {%- endif %} Visit Zama
{%- block extrabody %} {% endblock %} @@ -236,24 +256,6 @@ }); - {#- Do not conflict with RTD insertion of analytics script #} - {%- if not READTHEDOCS %} - {%- if theme_analytics_id %} - - - - - {%- endif %} - {%- endif %} - {%- block footer %} {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 158558e83..8bc6aa66a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -66,14 +66,20 @@ nbsphinx_codecell_lexer = 'ipython3' # html_theme = "sphinx_rtd_theme" html_style = "css/zama.css" -html_logo = "logo-black.png" +html_logo = "_static/logo.png" +html_favicon = "_static/favicon.ico" html_theme_options = { "logo_only": False, + "analytics_id": "G-XRM93J9QBW", + "collapse_navigation": True, } +pygments_style = "zenburn" html_last_updated_fmt = None # '%b %d, %Y' html_show_copyright = True html_show_sphinx = False -pygments_style = "zenburn" +html_context = { + "show_docs_home_link": True, # Show/hide docs link in top menu +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, From 286dda79b2f7e6f1108d412bd62ae0698b077d7b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 10:58:15 +0200 Subject: [PATCH 0398/1104] fix: correct pytest command in Makefile --- Makefile | 2 +- tests/conftest.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8b9719eda..b5d680a3c 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ pcc_internal: $(PCC_DEPS) pytest: poetry run pytest -svv \ - --global-coverage-infos-json global-coverage-infos.json \ + --global-coverage-infos-json=global-coverage-infos.json \ --cov=$(SRC_DIR) --cov-fail-under=100 \ --cov-report=term-missing:skip-covered tests/ .PHONY: pytest diff --git a/tests/conftest.py b/tests/conftest.py index 5b41347e4..73acdbb9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,6 +26,8 @@ def pytest_addoption(parser): parser.addoption( "--global-coverage-infos-json", + action="store", + default=None, type=str, help="To dump pytest-cov term report to a text file.", ) From dedbde93d077cdd589d53c7c047d6eac92b47eec Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 11 Oct 2021 14:49:16 +0200 Subject: [PATCH 0399/1104] feat: get_printable_graph is more precise get_printable_graph prints the constant if the ArbitraryFunction has a baked constant closes #584 --- concrete/common/debugging/printing.py | 45 ++++++++++++++--- tests/numpy/test_compile.py | 6 +-- tests/numpy/test_debugging.py | 73 ++++++++++++++++++++------- tests/numpy/test_tracing.py | 56 ++++++++++---------- 4 files changed, 126 insertions(+), 54 deletions(-) diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index fc367a2c1..0b0444e7f 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -22,6 +22,23 @@ def output_data_type_to_string(node): return ", ".join([str(o) for o in node.outputs]) +def shorten_a_constant(constant_data: str): + """Return a constant (if small) or an extra of the constant (if too large). + + Args: + constant (str): The constant we want to shorten + + Returns: + str: a string to represent the constant + """ + + content = str(constant_data).replace("\n", "") + # if content is longer than 25 chars, only show the first and the last 10 chars of it + # 25 is selected using the spaces available before data type information + short_content = f"{content[:10]} ... {content[-10:]}" if len(content) > 25 else content + return short_content + + def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: """Return a string representing a graph. @@ -52,10 +69,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: if isinstance(node, Input): what_to_print = node.input_name elif isinstance(node, Constant): - content = str(node.constant_data).replace("\n", "") - # if content is longer than 25 chars, only show the first and the last 10 chars of it - # 25 is selected using the spaces available before data type information - to_show = f"{content[:10]} ... {content[-10:]}" if len(content) > 25 else content + to_show = shorten_a_constant(node.constant_data) what_to_print = f"Constant({to_show})" else: @@ -81,15 +95,34 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: list_of_arg_name.sort() custom_assert([x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name)))) + prefix_to_add_to_what_to_print = "" + suffix_to_add_to_what_to_print = "" + + # Print constant that may be in the UnivariateFunction. For the moment, it considers + # there is a single constant maximally and that there is 2 inputs maximally + if isinstance(node, UnivariateFunction) and "baked_constant" in node.op_kwargs: + baked_constant = node.op_kwargs["baked_constant"] + if node.op_attributes["in_which_input_is_constant"] == 0: + prefix_to_add_to_what_to_print = f"{shorten_a_constant(baked_constant)}, " + else: + custom_assert( + node.op_attributes["in_which_input_is_constant"] == 1, + "'in_which_input_is_constant' should be a key of node.op_attributes", + ) + suffix_to_add_to_what_to_print = f", {shorten_a_constant(baked_constant)}" + # Then, just print the predecessors in the right order - what_to_print += ", ".join(["%" + x[1] for x in list_of_arg_name]) + ")" + what_to_print += prefix_to_add_to_what_to_print + what_to_print += ", ".join(["%" + x[1] for x in list_of_arg_name]) + what_to_print += suffix_to_add_to_what_to_print + what_to_print += ")" # This code doesn't work with more than a single output new_line = f"%{i} = {what_to_print}" # Manage datatypes if show_data_types: - new_line = f"{new_line: <40s} # {output_data_type_to_string(node)}" + new_line = f"{new_line: <50s} # {output_data_type_to_string(node)}" returned_str += f"{new_line}\n" diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index f0c5aa948..eb155c776 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -360,11 +360,11 @@ def test_small_inputset_treat_warnings_as_errors(): (4,), # Remark that, when you do the dot of tensors of 4 values between 0 and 3, # you can get a maximal value of 4*3*3 = 36, ie something on 6 bits - "%0 = x " + "%0 = x " "# EncryptedTensor, shape=(4,)>" - "\n%1 = y " + "\n%1 = y " "# EncryptedTensor, shape=(4,)>" - "\n%2 = Dot(%0, %1) " + "\n%2 = Dot(%0, %1) " "# EncryptedScalar>" "\nreturn(%2)\n", ), diff --git a/tests/numpy/test_debugging.py b/tests/numpy/test_debugging.py index b19ab4aff..267973841 100644 --- a/tests/numpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -112,6 +112,14 @@ def issue_130_c(x, y): issue_130_c, "%0 = Constant(1)\n%1 = x\n%2 = Sub(%0, %1)\nreturn(%2, %2)\n", ), + ( + lambda x, y: numpy.arctan2(x, 42) + y, + "%0 = y\n%1 = x\n%2 = np.arctan2(%1, 42)\n%3 = Add(%2, %0)\nreturn(%3)\n", + ), + ( + lambda x, y: numpy.arctan2(43, x) + y, + "%0 = y\n%1 = x\n%2 = np.arctan2(43, %1)\n%3 = Add(%2, %0)\nreturn(%3)\n", + ), ], ) @pytest.mark.parametrize( @@ -221,9 +229,12 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): EncryptedScalar(Integer(64, is_signed=False)), EncryptedScalar(Integer(32, is_signed=True)), ), - "%0 = x # EncryptedScalar>" - "\n%1 = y # EncryptedScalar>" - "\n%2 = Add(%0, %1) # EncryptedScalar>" + "%0 = x " + "# EncryptedScalar>" + "\n%1 = y " + " # EncryptedScalar>" + "\n%2 = Add(%0, %1) " + " # EncryptedScalar>" "\nreturn(%2)\n", ), ( @@ -232,11 +243,11 @@ def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): EncryptedScalar(Integer(17, is_signed=False)), EncryptedScalar(Integer(23, is_signed=False)), ), - "%0 = x " + "%0 = x " "# EncryptedScalar>" - "\n%1 = y " + "\n%1 = y " "# EncryptedScalar>" - "\n%2 = Mul(%0, %1) " + "\n%2 = Mul(%0, %1) " "# EncryptedScalar>" "\nreturn(%2)\n", ), @@ -262,37 +273,37 @@ def test_print_with_show_data_types(lambda_f, x_y, ref_graph_str): ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[x], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x " + "%0 = x " "# EncryptedScalar>" - "\n%1 = TLU(%0) " + "\n%1 = TLU(%0) " "# EncryptedScalar>" "\nreturn(%1)\n", ), ( lambda x: LOOKUP_TABLE_FROM_3B_TO_2B[x + 4], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x " + "%0 = x " "# EncryptedScalar>" - "\n%1 = Constant(4) " + "\n%1 = Constant(4) " "# ClearScalar>" - "\n%2 = Add(%0, %1) " + "\n%2 = Add(%0, %1) " "# EncryptedScalar>" - "\n%3 = TLU(%2) " + "\n%3 = TLU(%2) " "# EncryptedScalar>" "\nreturn(%3)\n", ), ( lambda x: LOOKUP_TABLE_FROM_2B_TO_4B[LOOKUP_TABLE_FROM_3B_TO_2B[x + 4]], {"x": EncryptedScalar(Integer(2, is_signed=False))}, - "%0 = x " + "%0 = x " "# EncryptedScalar>" - "\n%1 = Constant(4) " + "\n%1 = Constant(4) " "# ClearScalar>" - "\n%2 = Add(%0, %1) " + "\n%2 = Add(%0, %1) " "# EncryptedScalar>" - "\n%3 = TLU(%2) " + "\n%3 = TLU(%2) " "# EncryptedScalar>" - "\n%4 = TLU(%3) " + "\n%4 = TLU(%3) " "# EncryptedScalar>" "\nreturn(%4)\n", ), @@ -311,3 +322,31 @@ def test_print_with_show_data_types_with_direct_tlu(lambda_f, params, ref_graph_ f"==================\nExpected \n{ref_graph_str}" f"==================\n" ) + + +def test_numpy_long_constant(): + "Test get_printable_graph with long constant" + + def all_explicit_operations(x): + intermediate = numpy.add(x, numpy.arange(100).reshape(10, 10)) + intermediate = numpy.subtract(intermediate, numpy.arange(10).reshape(1, 10)) + intermediate = numpy.arctan2(numpy.arange(10, 20).reshape(1, 10), intermediate) + intermediate = numpy.arctan2(numpy.arange(100, 200).reshape(10, 10), intermediate) + return intermediate + + op_graph = tracing.trace_numpy_function( + all_explicit_operations, {"x": EncryptedTensor(Integer(32, True), shape=(10, 10))} + ) + + expected = """ +%0 = Constant([[0 1 2 3 4 5 6 7 8 9]]) # ClearTensor, shape=(1, 10)> +%1 = x # EncryptedTensor, shape=(10, 10)> +%2 = Constant([[ 0 1 2 ... 97 98 99]]) # ClearTensor, shape=(10, 10)> +%3 = Add(%1, %2) # EncryptedTensor, shape=(10, 10)> +%4 = Sub(%3, %0) # EncryptedTensor, shape=(10, 10)> +%5 = np.arctan2([[10 11 12 ... 17 18 19]], %4) # EncryptedTensor, shape=(10, 10)> +%6 = np.arctan2([[100 101 ... 198 199]], %5) # EncryptedTensor, shape=(10, 10)> +return(%6) +""".lstrip() # noqa: E501 + + assert get_printable_graph(op_graph, show_data_types=True) == expected diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index e07f73fb9..c7fa35a3e 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -188,21 +188,21 @@ def test_numpy_tracing_tensors(): ) expected = """ -%0 = Constant([[2 1] [1 2]]) # ClearTensor, shape=(2, 2)> -%1 = Constant([[1 2] [2 1]]) # ClearTensor, shape=(2, 2)> -%2 = Constant([[10 20] [30 40]]) # ClearTensor, shape=(2, 2)> -%3 = Constant([[100 200] [300 400]]) # ClearTensor, shape=(2, 2)> -%4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> -%5 = x # EncryptedTensor, shape=(2, 2)> -%6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> -%7 = Add(%5, %6) # EncryptedTensor, shape=(2, 2)> -%8 = Add(%4, %7) # EncryptedTensor, shape=(2, 2)> -%9 = Sub(%3, %8) # EncryptedTensor, shape=(2, 2)> -%10 = Sub(%9, %2) # EncryptedTensor, shape=(2, 2)> -%11 = Mul(%10, %1) # EncryptedTensor, shape=(2, 2)> -%12 = Mul(%0, %11) # EncryptedTensor, shape=(2, 2)> +%0 = Constant([[2 1] [1 2]]) # ClearTensor, shape=(2, 2)> +%1 = Constant([[1 2] [2 1]]) # ClearTensor, shape=(2, 2)> +%2 = Constant([[10 20] [30 40]]) # ClearTensor, shape=(2, 2)> +%3 = Constant([[100 200] [300 400]]) # ClearTensor, shape=(2, 2)> +%4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> +%5 = x # EncryptedTensor, shape=(2, 2)> +%6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> +%7 = Add(%5, %6) # EncryptedTensor, shape=(2, 2)> +%8 = Add(%4, %7) # EncryptedTensor, shape=(2, 2)> +%9 = Sub(%3, %8) # EncryptedTensor, shape=(2, 2)> +%10 = Sub(%9, %2) # EncryptedTensor, shape=(2, 2)> +%11 = Mul(%10, %1) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(%0, %11) # EncryptedTensor, shape=(2, 2)> return(%12) -""".lstrip() +""".lstrip() # noqa: E501 assert get_printable_graph(op_graph, show_data_types=True) == expected @@ -227,21 +227,21 @@ def test_numpy_explicit_tracing_tensors(): ) expected = """ -%0 = Constant([[2 1] [1 2]]) # ClearTensor, shape=(2, 2)> -%1 = Constant([[1 2] [2 1]]) # ClearTensor, shape=(2, 2)> -%2 = Constant([[10 20] [30 40]]) # ClearTensor, shape=(2, 2)> -%3 = Constant([[100 200] [300 400]]) # ClearTensor, shape=(2, 2)> -%4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> -%5 = x # EncryptedTensor, shape=(2, 2)> -%6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> -%7 = Add(%5, %6) # EncryptedTensor, shape=(2, 2)> -%8 = Add(%4, %7) # EncryptedTensor, shape=(2, 2)> -%9 = Sub(%3, %8) # EncryptedTensor, shape=(2, 2)> -%10 = Sub(%9, %2) # EncryptedTensor, shape=(2, 2)> -%11 = Mul(%10, %1) # EncryptedTensor, shape=(2, 2)> -%12 = Mul(%0, %11) # EncryptedTensor, shape=(2, 2)> +%0 = Constant([[2 1] [1 2]]) # ClearTensor, shape=(2, 2)> +%1 = Constant([[1 2] [2 1]]) # ClearTensor, shape=(2, 2)> +%2 = Constant([[10 20] [30 40]]) # ClearTensor, shape=(2, 2)> +%3 = Constant([[100 200] [300 400]]) # ClearTensor, shape=(2, 2)> +%4 = Constant([[5 6] [7 8]]) # ClearTensor, shape=(2, 2)> +%5 = x # EncryptedTensor, shape=(2, 2)> +%6 = Constant([[1 2] [3 4]]) # ClearTensor, shape=(2, 2)> +%7 = Add(%5, %6) # EncryptedTensor, shape=(2, 2)> +%8 = Add(%4, %7) # EncryptedTensor, shape=(2, 2)> +%9 = Sub(%3, %8) # EncryptedTensor, shape=(2, 2)> +%10 = Sub(%9, %2) # EncryptedTensor, shape=(2, 2)> +%11 = Mul(%10, %1) # EncryptedTensor, shape=(2, 2)> +%12 = Mul(%0, %11) # EncryptedTensor, shape=(2, 2)> return(%12) -""".lstrip() +""".lstrip() # noqa: E501 assert get_printable_graph(op_graph, show_data_types=True) == expected From 4205e096f48205c13de9e731cefc7fc8c946add1 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 11 Oct 2021 18:04:51 +0300 Subject: [PATCH 0400/1104] feat(benchmark): integrate alert system into measurement script --- script/progress_tracker_utils/measure.py | 65 ++++++++++++++++++++---- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index e95529a26..231e0f087 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -27,7 +27,48 @@ def name_to_id(name): return urllib.parse.quote_plus(name.lower()) -def identify_metrics(script, lines, metrics): +def register_alert(script, index, line, metrics, alerts): + """Parse line, check its correctness, add it to list of alerts if it's valid""" + + # Extract the alert details + alert_line = line.replace("# Alert:", "") + + # Parse the alert and append it to list of alerts + supported_operators = ["==", "!=", "<=", ">=", "<", ">"] + for operator in supported_operators: + alert_details = alert_line.split(operator) + + # An alert should be of form `{metric} {operator} {constant}` + if len(alert_details) == 2: + metric_label = alert_details[0].strip() + metric_id = name_to_id(metric_label) + + if metric_id not in metrics: + raise SyntaxError( + f"An alert is using an undefined metric `{metric_label}` " + f"(at line {index + 1} of {script})", + ) + + value_str = alert_details[1].strip() + try: + value = float(value_str) + alerts.append({"metric": metric_id, "comparison": operator, "value": value}) + except ValueError as error: + raise SyntaxError( + f"An alert is not using a constant floating point for comparison " + f"(at line {index + 1} of {script})", + ) from error + + break + else: + raise SyntaxError( + f"An alert is not using any of the supported comparisons " + f"{', '.join(supported_operators)} " + f"(at line {index + 1} of {script})", + ) + + +def identify_metrics_and_alerts(script, lines, metrics, alerts): """Identify the metrics of a script and make sure the annotations are well-formed""" # Create a flag to detect `# Measure: End` without a measurement start @@ -90,6 +131,8 @@ def identify_metrics(script, lines, metrics): in_measurement = True measurement_line = index + 1 measurement_indentation = indentation + elif line.startswith("# Alert:"): + register_alert(script, index, line, metrics, alerts) # Make sure there isn't an active measurement that hasn't finished if in_measurement: @@ -307,18 +350,14 @@ def main(): if target_id in result["targets"]: raise RuntimeError(f"Target `{target_name}` is already registered") - # Create an entry in the result for the current target - result["targets"][target_id] = { - "name": target_name, - "measurements": {}, - "code": "\n".join(lines), - } - # Create a dictionary to hold `metric_id` to `metric_name` metrics = {} + # Create a list to hold alerts in form { "metric": ..., "comparison": ..., "value": ... } + alerts = [] + # Identify metrics of the current script - identify_metrics(path, lines, metrics) + identify_metrics_and_alerts(path, lines, metrics, alerts) # Extract the script name name = os.path.basename(path) @@ -326,6 +365,14 @@ def main(): # Create another script to hold the modified version of the current script create_modified_script(name, lines, metrics) + # Create an entry in the result for the current target + result["targets"][target_id] = { + "name": target_name, + "measurements": {}, + "alerts": alerts, + "code": "\n".join(lines), + } + # Perform and save measurements perform_measurements(path, name, target_id, metrics, samples, result) From c8b95de3e4df7a54ef840b471c13163a335627f4 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 11 Oct 2021 18:04:54 +0300 Subject: [PATCH 0401/1104] feat(benchmark): add alerts to benchmarks --- benchmarks/124_minus_x.py | 1 + benchmarks/124_minus_x_tensor.py | 1 + benchmarks/linear_regression.py | 1 + benchmarks/logistic_regression.py | 1 + benchmarks/single_table_lookup.py | 1 + benchmarks/x_minus_1_2_3.py | 1 + benchmarks/x_minus_1_2_3_broadcasted.py | 1 + benchmarks/x_minus_24.py | 1 + benchmarks/x_minus_24_tensor.py | 1 + benchmarks/x_minus_y.py | 1 + benchmarks/x_minus_y_broadcasted_tensors.py | 1 + benchmarks/x_minus_y_tensor_and_scalar.py | 1 + benchmarks/x_minus_y_tensors.py | 1 + benchmarks/x_plus_1_2_3.py | 1 + benchmarks/x_plus_1_2_3_broadcasted.py | 1 + benchmarks/x_plus_42.py | 1 + benchmarks/x_plus_42_tensor.py | 1 + benchmarks/x_plus_y.py | 1 + benchmarks/x_plus_y_broadcasted_tensors.py | 1 + benchmarks/x_plus_y_tensor_and_scalar.py | 1 + benchmarks/x_plus_y_tensors.py | 1 + benchmarks/x_times_1_2_3.py | 1 + benchmarks/x_times_1_2_3_broadcasted.py | 1 + benchmarks/x_times_7.py | 1 + benchmarks/x_times_7_tensor.py | 1 + benchmarks/x_times_y.py | 1 + benchmarks/x_times_y_broadcasted_tensors.py | 1 + benchmarks/x_times_y_tensor_and_scalar.py | 1 + benchmarks/x_times_y_tensors.py | 1 + benchmarks/x_to_the_power_of_2.py | 1 + 30 files changed, 30 insertions(+) diff --git a/benchmarks/124_minus_x.py b/benchmarks/124_minus_x.py index 1b8e4f13f..fb192944e 100644 --- a/benchmarks/124_minus_x.py +++ b/benchmarks/124_minus_x.py @@ -40,6 +40,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/124_minus_x_tensor.py b/benchmarks/124_minus_x_tensor.py index ed91bc310..eab8fcafe 100644 --- a/benchmarks/124_minus_x_tensor.py +++ b/benchmarks/124_minus_x_tensor.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index c74b2d0b6..e71f63cb7 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -191,6 +191,7 @@ def main(): # Measure: Non Homomorphic Loss = non_homomorphic_loss # Measure: Homomorphic Loss = homomorphic_loss # Measure: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference + # Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 20 if __name__ == "__main__": diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index 664ac700b..1d7de17c3 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -264,6 +264,7 @@ def main(): print(f"Accuracy: {accuracy:.2f}%") # Measure: Accuracy (%) = accuracy + # Alert: Accuracy (%) < 85 if __name__ == "__main__": diff --git a/benchmarks/single_table_lookup.py b/benchmarks/single_table_lookup.py index 98f3018a6..0f0541398 100644 --- a/benchmarks/single_table_lookup.py +++ b/benchmarks/single_table_lookup.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_1_2_3.py b/benchmarks/x_minus_1_2_3.py index 746f69d39..0e696dc8a 100644 --- a/benchmarks/x_minus_1_2_3.py +++ b/benchmarks/x_minus_1_2_3.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_1_2_3_broadcasted.py b/benchmarks/x_minus_1_2_3_broadcasted.py index b78e4a2f4..f1473f062 100644 --- a/benchmarks/x_minus_1_2_3_broadcasted.py +++ b/benchmarks/x_minus_1_2_3_broadcasted.py @@ -43,6 +43,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_24.py b/benchmarks/x_minus_24.py index 9a39d0b22..3bca798d1 100644 --- a/benchmarks/x_minus_24.py +++ b/benchmarks/x_minus_24.py @@ -40,6 +40,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_24_tensor.py b/benchmarks/x_minus_24_tensor.py index b88be1f8d..3961c94bd 100644 --- a/benchmarks/x_minus_24_tensor.py +++ b/benchmarks/x_minus_24_tensor.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y.py b/benchmarks/x_minus_y.py index 47c24a09e..b7d5db488 100644 --- a/benchmarks/x_minus_y.py +++ b/benchmarks/x_minus_y.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y_broadcasted_tensors.py b/benchmarks/x_minus_y_broadcasted_tensors.py index d586a8ab2..808575c49 100644 --- a/benchmarks/x_minus_y_broadcasted_tensors.py +++ b/benchmarks/x_minus_y_broadcasted_tensors.py @@ -46,6 +46,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y_tensor_and_scalar.py b/benchmarks/x_minus_y_tensor_and_scalar.py index 27e825ca4..8e69743b9 100644 --- a/benchmarks/x_minus_y_tensor_and_scalar.py +++ b/benchmarks/x_minus_y_tensor_and_scalar.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y_tensors.py b/benchmarks/x_minus_y_tensors.py index 5932e97c2..3858563f3 100644 --- a/benchmarks/x_minus_y_tensors.py +++ b/benchmarks/x_minus_y_tensors.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_1_2_3.py b/benchmarks/x_plus_1_2_3.py index e9339d97f..7503d78cd 100644 --- a/benchmarks/x_plus_1_2_3.py +++ b/benchmarks/x_plus_1_2_3.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_1_2_3_broadcasted.py b/benchmarks/x_plus_1_2_3_broadcasted.py index 0095cb8c1..36132d3d6 100644 --- a/benchmarks/x_plus_1_2_3_broadcasted.py +++ b/benchmarks/x_plus_1_2_3_broadcasted.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index 95f7be9ed..d74ff023e 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -40,6 +40,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_42_tensor.py b/benchmarks/x_plus_42_tensor.py index 6ca272380..a709b788e 100644 --- a/benchmarks/x_plus_42_tensor.py +++ b/benchmarks/x_plus_42_tensor.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y.py b/benchmarks/x_plus_y.py index a4c809b9c..39aae0377 100644 --- a/benchmarks/x_plus_y.py +++ b/benchmarks/x_plus_y.py @@ -42,6 +42,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y_broadcasted_tensors.py b/benchmarks/x_plus_y_broadcasted_tensors.py index 91756ec8b..b2408d13f 100644 --- a/benchmarks/x_plus_y_broadcasted_tensors.py +++ b/benchmarks/x_plus_y_broadcasted_tensors.py @@ -46,6 +46,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y_tensor_and_scalar.py b/benchmarks/x_plus_y_tensor_and_scalar.py index c44dd9e81..94224b449 100644 --- a/benchmarks/x_plus_y_tensor_and_scalar.py +++ b/benchmarks/x_plus_y_tensor_and_scalar.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y_tensors.py b/benchmarks/x_plus_y_tensors.py index 4508d2ecc..836d89c97 100644 --- a/benchmarks/x_plus_y_tensors.py +++ b/benchmarks/x_plus_y_tensors.py @@ -46,6 +46,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_1_2_3.py b/benchmarks/x_times_1_2_3.py index 1f3b4bc85..040297c88 100644 --- a/benchmarks/x_times_1_2_3.py +++ b/benchmarks/x_times_1_2_3.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_1_2_3_broadcasted.py b/benchmarks/x_times_1_2_3_broadcasted.py index cefe9f8ec..77d56aa57 100644 --- a/benchmarks/x_times_1_2_3_broadcasted.py +++ b/benchmarks/x_times_1_2_3_broadcasted.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_7.py b/benchmarks/x_times_7.py index f5338bfb5..156aafa1d 100644 --- a/benchmarks/x_times_7.py +++ b/benchmarks/x_times_7.py @@ -40,6 +40,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_7_tensor.py b/benchmarks/x_times_7_tensor.py index 7798ad568..c92d02031 100644 --- a/benchmarks/x_times_7_tensor.py +++ b/benchmarks/x_times_7_tensor.py @@ -41,6 +41,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y.py b/benchmarks/x_times_y.py index 562289db3..50dc97d13 100644 --- a/benchmarks/x_times_y.py +++ b/benchmarks/x_times_y.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y_broadcasted_tensors.py b/benchmarks/x_times_y_broadcasted_tensors.py index a804ad773..c830ea77c 100644 --- a/benchmarks/x_times_y_broadcasted_tensors.py +++ b/benchmarks/x_times_y_broadcasted_tensors.py @@ -46,6 +46,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y_tensor_and_scalar.py b/benchmarks/x_times_y_tensor_and_scalar.py index 906c31fa1..7b7bad7e4 100644 --- a/benchmarks/x_times_y_tensor_and_scalar.py +++ b/benchmarks/x_times_y_tensor_and_scalar.py @@ -45,6 +45,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y_tensors.py b/benchmarks/x_times_y_tensors.py index e4e5976c0..7d561eb4a 100644 --- a/benchmarks/x_times_y_tensors.py +++ b/benchmarks/x_times_y_tensors.py @@ -46,6 +46,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_to_the_power_of_2.py b/benchmarks/x_to_the_power_of_2.py index da3505f64..4391f6ad0 100644 --- a/benchmarks/x_to_the_power_of_2.py +++ b/benchmarks/x_to_the_power_of_2.py @@ -40,6 +40,7 @@ def main(): correct += 1 # Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # Alert: Accuracy (%) != 100 if __name__ == "__main__": From 1a051e64d595af7a7a668ea655c49e614d821e57 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Tue, 12 Oct 2021 11:11:24 +0200 Subject: [PATCH 0402/1104] chore: update docs theme to v0.5.0 --- docs/_static/css/zama.css | 96 +++++++++++++++++++++++++++++++++++-- docs/_templates/layout.html | 50 ++++++++++++++++--- docs/conf.py | 1 - 3 files changed, 134 insertions(+), 13 deletions(-) diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index a80d41c2b..c6bc81012 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -12,6 +12,8 @@ --tertiary-color: #414042; --tertiary-color-light: #e6e7e8; --link-color: black; + --button-accept-color: #ffd208; + --button-decline-color: grey; --primary-font: Archivo, sans-serif; } @@ -212,11 +214,9 @@ dl.class > dt:first-of-type { display: block !important; } - - - /* - * Admonitions - */ +/* + * Admonitions + */ /* Important admonition */ .rst-content .important .admonition-title { @@ -246,3 +246,89 @@ dl.class > dt:first-of-type { .rst-content .note { background: var(--tertiary-color-light); } + +/* + * RGPD + */ + +.rgpd { + z-index: 300; + position: fixed; + width: calc(100% - 40px); + height: 76px; + padding: 17px 0px; + border: 1px solid rgba(0, 0, 0, 0.2); + background-color: rgba(255, 255, 255, 0.9); + bottom: 20px; + left: 20px; + font-size: 16px; + padding-left: 12.5%; + padding-right: 12.5%; + background: black; + color: white; + opacity: 75%; + font-size: 17px; + white-space: nowrap; +} + +.zama-container { + max-width: 1440px; + margin: auto; + width: 100%; +} + +.rgpd .zama-container .padded { + display: flex; + align-items: center; + justify-content: center; +} + +.rgpd .consent { + text-overflow: ellipsis; + overflow: hidden; + +} + +.rgpd .button { + text-decoration: none; + white-space: nowrap; + position: relative; + z-index: 0; + display: inline-flex; + align-items: center; + justify-content: center; + height: 60px; + padding: 0px 10px; + margin-left: 15px; + height: 46px; + text-transform: uppercase; + font-family: var(--primary-font); + font-weight: bold; + -webkit-font-smoothing: antialiased; + font-size: 14px; + cursor: pointer; + color: black; + transition: all 0.33s cubic-bezier(0.33, 0, 0, 1); +} + +.rgpd .button::after { + background-color: white; + height: 46px; + content: ''; + display: block; + position: absolute; + right: 0; + top: 3px; + width: 100%; + height: 40px; + border-radius: 25px; + z-index: -1; +} + +.rgpd .button-accept::after{ + background-color: var(--button-accept-color); +} + +.rgpd .button-decline::after{ + background-color: var(--button-decline-color); +} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 8c127267f..f6769705e 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -16,7 +16,19 @@ - + + + + + + {#- Do not conflict with RTD insertion of analytics script #} {%- if not READTHEDOCS %} {%- if theme_analytics_id %} @@ -250,13 +262,37 @@ {% include "versions.html" -%} - - {%- block footer %} {% endblock %} + {%- if theme_analytics_id %} +
+
+
+ +
Accept
+
Decline
+ +
+
+
+ {%- endif%} diff --git a/docs/conf.py b/docs/conf.py index 8bc6aa66a..66a814edc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,7 +71,6 @@ html_favicon = "_static/favicon.ico" html_theme_options = { "logo_only": False, "analytics_id": "G-XRM93J9QBW", - "collapse_navigation": True, } pygments_style = "zenburn" html_last_updated_fmt = None # '%b %d, %Y' From e7a141c4f118486254a858eb182d059743c4bba7 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 12 Oct 2021 14:59:26 +0300 Subject: [PATCH 0403/1104] feat(ci): add more optional scopes --- .github/workflows/continuous-integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index c432b7a63..f3ba051e8 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -195,10 +195,10 @@ jobs: if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: - pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values)\))?\:) .+$' + pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts)\))?\:) .+$' flags: 'gs' error: "Your first line has to contain a commit type and scope like \"feat(my_feature): msg\".\ - Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values)\\))?\\:)'" + Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts)\\))?\\:)'" excludeDescription: 'true' # optional: this excludes the description body of a pull request excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request From 31e14e7b660483eca58784aa0f6e155c5a2b5bd8 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 14:16:25 +0200 Subject: [PATCH 0404/1104] chore(ci): fix another issue with a glob in the release workflow --- .github/workflows/continuous-integration.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index f3ba051e8..6eb086dd9 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -546,11 +546,11 @@ jobs: - name: Create directory for artifacts if: ${{ success() && !cancelled() }} run: | - ARTIFACTS_RAW_DIR=/tmp/release_artifacts/raw/ + ARTIFACTS_RAW_DIR=/tmp/release_artifacts/raw mkdir -p "${ARTIFACTS_RAW_DIR}" echo "ARTIFACTS_RAW_DIR=${ARTIFACTS_RAW_DIR}" >> "$GITHUB_ENV" - ARTIFACTS_PACKAGED_DIR=/tmp/release_artifacts/packaged/ + ARTIFACTS_PACKAGED_DIR=/tmp/release_artifacts/packaged mkdir -p "${ARTIFACTS_PACKAGED_DIR}" echo "ARTIFACTS_PACKAGED_DIR=${ARTIFACTS_PACKAGED_DIR}" >> "$GITHUB_ENV" - name: Download Documentation @@ -578,6 +578,7 @@ jobs: tar -cvzf "${ARTIFACTS_PACKAGED_DIR}/html-docs.tar.gz" ./* popd cp "${RAW_CHANGELOG_DIR}"/* "${ARTIFACTS_PACKAGED_DIR}" + ls -a . - name: Create GitHub release if: ${{ success() && !cancelled() }} id: create-release @@ -588,7 +589,7 @@ jobs: **Documentation:** https://docs.zama.ai/concrete/ prerelease: ${{ fromJSON(env.IS_PRERELEASE) }} files: | - '${{ env.ARTIFACTS_PACKAGED_DIR }}/*' + ${{ env.ARTIFACTS_PACKAGED_DIR }}/* tag_name: ${{ env.GIT_TAG }} fail_on_unmatched_files: true token: ${{ secrets.BOT_TOKEN }} From fc9fc992c864ece70eb4bb31434d6428bba41a33 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 14:40:02 +0200 Subject: [PATCH 0405/1104] fix(tracing): do not process already visited tracers - some topologies triggered a bug where a tracer was visited several times - this created duplicate edges which caused problems later in the code - simply skip already visited tracers - add a check to see that all edges are indeed unique --- concrete/common/tracing/tracing_helpers.py | 12 +++++++++++- tests/numpy/test_compile.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index a2c894312..2e3eea6a8 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -6,7 +6,7 @@ from typing import Callable, Dict, Iterable, OrderedDict, Set, Type import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true, custom_assert from ..representation.intermediate import Input from ..values import BaseValue from .base_tracer import BaseTracer @@ -108,6 +108,8 @@ def create_graph_from_output_tracers( # use dict as ordered set next_tracers: Dict[BaseTracer, None] = {} for tracer in current_tracers: + if tracer in visited_tracers: + continue current_ir_node = tracer.traced_computation graph.add_node(current_ir_node) @@ -124,4 +126,12 @@ def create_graph_from_output_tracers( custom_assert(is_directed_acyclic_graph(graph)) + # Check each edge is unique + unique_edges = set( + (pred, succ, tuple((k, v) for k, v in edge_data.items())) + for pred, succ, edge_data in graph.edges(data=True) + ) + number_of_edges = len(graph.edges) + assert_true(len(unique_edges) == number_of_edges) + return graph diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index eb155c776..97563618e 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -38,6 +38,20 @@ def small_fused_table(x): return (10 * (numpy.cos(x + 1) + 1)).astype(numpy.uint32) +def complicated_topology(x): + """Mix x in an intricated way.""" + intermediate = x + x_p_1 = intermediate + 1 + x_p_2 = intermediate + 2 + x_p_3 = x_p_1 + x_p_2 + return ( + x_p_3.astype(numpy.int32), + x_p_2.astype(numpy.int32), + (x_p_2 + 3).astype(numpy.int32), + x_p_3.astype(numpy.int32) + 67, + ) + + @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ @@ -55,6 +69,7 @@ def small_fused_table(x): ["x", "y"], marks=pytest.mark.xfail(strict=True, raises=ValueError), ), + pytest.param(complicated_topology, ((0, 10),), ["x"]), ], ) def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_names): From 636da7808a3d21e6175c347b97b85c7acf572ee2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 15:42:48 +0200 Subject: [PATCH 0406/1104] chore: update version to 0.2.0-rc2 --- concrete/version.py | 2 +- docs/conf.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/concrete/version.py b/concrete/version.py index b5df944c9..193a1ba82 100644 --- a/concrete/version.py +++ b/concrete/version.py @@ -1,4 +1,4 @@ """Package version module.""" # Auto-generated by "make set_version" do not modify -__version__ = "0.2.0-rc1" +__version__ = "0.2.0-rc2" diff --git a/docs/conf.py b/docs/conf.py index 66a814edc..3500df8f9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ copyright = "2021, Zama" author = "Zama" # The full version, including alpha/beta/rc tags -release = "0.2.0-rc1" +release = "0.2.0-rc2" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d880b1809..4056d65cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.2.0-rc1" +version = "0.2.0-rc2" description = "Concrete Framework" authors = ["Zama "] packages = [ From 47f03e427fd37d206835753b52b9fa3414d7269c Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 12 Oct 2021 13:48:50 +0300 Subject: [PATCH 0407/1104] feat(benchmarks): add support for full and unit targets --- benchmarks/124_minus_x.py | 2 +- benchmarks/124_minus_x_tensor.py | 2 +- benchmarks/linear_regression.py | 2 +- benchmarks/logistic_regression.py | 2 +- benchmarks/single_table_lookup.py | 2 +- benchmarks/x_minus_1_2_3.py | 2 +- benchmarks/x_minus_1_2_3_broadcasted.py | 2 +- benchmarks/x_minus_24.py | 2 +- benchmarks/x_minus_24_tensor.py | 2 +- benchmarks/x_minus_y.py | 2 +- benchmarks/x_minus_y_broadcasted_tensors.py | 2 +- benchmarks/x_minus_y_tensor_and_scalar.py | 2 +- benchmarks/x_minus_y_tensors.py | 2 +- benchmarks/x_plus_1_2_3.py | 2 +- benchmarks/x_plus_1_2_3_broadcasted.py | 2 +- benchmarks/x_plus_42.py | 2 +- benchmarks/x_plus_42_tensor.py | 2 +- benchmarks/x_plus_y.py | 2 +- benchmarks/x_plus_y_broadcasted_tensors.py | 2 +- benchmarks/x_plus_y_tensor_and_scalar.py | 2 +- benchmarks/x_plus_y_tensors.py | 2 +- benchmarks/x_times_1_2_3.py | 2 +- benchmarks/x_times_1_2_3_broadcasted.py | 2 +- benchmarks/x_times_7.py | 2 +- benchmarks/x_times_7_tensor.py | 2 +- benchmarks/x_times_y.py | 2 +- benchmarks/x_times_y_broadcasted_tensors.py | 2 +- benchmarks/x_times_y_tensor_and_scalar.py | 2 +- benchmarks/x_times_y_tensors.py | 2 +- benchmarks/x_to_the_power_of_2.py | 2 +- script/progress_tracker_utils/measure.py | 31 +++++++++++++-------- 31 files changed, 49 insertions(+), 42 deletions(-) diff --git a/benchmarks/124_minus_x.py b/benchmarks/124_minus_x.py index fb192944e..aa74101d0 100644 --- a/benchmarks/124_minus_x.py +++ b/benchmarks/124_minus_x.py @@ -1,4 +1,4 @@ -# Target: 124 - x +# Unit Target: 124 - x import random diff --git a/benchmarks/124_minus_x_tensor.py b/benchmarks/124_minus_x_tensor.py index eab8fcafe..8b2db4834 100644 --- a/benchmarks/124_minus_x_tensor.py +++ b/benchmarks/124_minus_x_tensor.py @@ -1,4 +1,4 @@ -# Target: 124 - x (Tensor) +# Unit Target: 124 - x (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index e71f63cb7..0612456d0 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -1,4 +1,4 @@ -# Target: Linear Regression +# Full Target: Linear Regression # Disable line length warnings as we have a looooong metric... # flake8: noqa: E501 diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index 1d7de17c3..79eeb3a0a 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -1,4 +1,4 @@ -# Target: Logistic Regression +# Full Target: Logistic Regression import numpy as np import torch diff --git a/benchmarks/single_table_lookup.py b/benchmarks/single_table_lookup.py index 0f0541398..2b650319a 100644 --- a/benchmarks/single_table_lookup.py +++ b/benchmarks/single_table_lookup.py @@ -1,4 +1,4 @@ -# Target: Single Table Lookup +# Unit Target: Single Table Lookup import random diff --git a/benchmarks/x_minus_1_2_3.py b/benchmarks/x_minus_1_2_3.py index 0e696dc8a..9b1f59071 100644 --- a/benchmarks/x_minus_1_2_3.py +++ b/benchmarks/x_minus_1_2_3.py @@ -1,4 +1,4 @@ -# Target: x - [1, 2, 3] +# Unit Target: x - [1, 2, 3] import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_minus_1_2_3_broadcasted.py b/benchmarks/x_minus_1_2_3_broadcasted.py index f1473f062..64f583384 100644 --- a/benchmarks/x_minus_1_2_3_broadcasted.py +++ b/benchmarks/x_minus_1_2_3_broadcasted.py @@ -1,4 +1,4 @@ -# Target: x - [1, 2, 3] (Broadcasted) +# Unit Target: x - [1, 2, 3] (Broadcasted) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_minus_24.py b/benchmarks/x_minus_24.py index 3bca798d1..2b03e8d8d 100644 --- a/benchmarks/x_minus_24.py +++ b/benchmarks/x_minus_24.py @@ -1,4 +1,4 @@ -# Target: x - 24 +# Unit Target: x - 24 import random diff --git a/benchmarks/x_minus_24_tensor.py b/benchmarks/x_minus_24_tensor.py index 3961c94bd..06bd641b7 100644 --- a/benchmarks/x_minus_24_tensor.py +++ b/benchmarks/x_minus_24_tensor.py @@ -1,4 +1,4 @@ -# Target: x - 24 (Tensor) +# Unit Target: x - 24 (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_minus_y.py b/benchmarks/x_minus_y.py index b7d5db488..3f6413bda 100644 --- a/benchmarks/x_minus_y.py +++ b/benchmarks/x_minus_y.py @@ -1,4 +1,4 @@ -# Target: x - y +# Unit Target: x - y import itertools import random diff --git a/benchmarks/x_minus_y_broadcasted_tensors.py b/benchmarks/x_minus_y_broadcasted_tensors.py index 808575c49..c9b7b9077 100644 --- a/benchmarks/x_minus_y_broadcasted_tensors.py +++ b/benchmarks/x_minus_y_broadcasted_tensors.py @@ -1,4 +1,4 @@ -# Target: x - y (Broadcasted Tensors) +# Unit Target: x - y (Broadcasted Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_minus_y_tensor_and_scalar.py b/benchmarks/x_minus_y_tensor_and_scalar.py index 8e69743b9..56f118d2d 100644 --- a/benchmarks/x_minus_y_tensor_and_scalar.py +++ b/benchmarks/x_minus_y_tensor_and_scalar.py @@ -1,4 +1,4 @@ -# Target: x - y (Tensor & Scalar) +# Unit Target: x - y (Tensor & Scalar) import random diff --git a/benchmarks/x_minus_y_tensors.py b/benchmarks/x_minus_y_tensors.py index 3858563f3..f6f1ad44c 100644 --- a/benchmarks/x_minus_y_tensors.py +++ b/benchmarks/x_minus_y_tensors.py @@ -1,4 +1,4 @@ -# Target: x - y (Tensors) +# Unit Target: x - y (Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_plus_1_2_3.py b/benchmarks/x_plus_1_2_3.py index 7503d78cd..2292e3460 100644 --- a/benchmarks/x_plus_1_2_3.py +++ b/benchmarks/x_plus_1_2_3.py @@ -1,4 +1,4 @@ -# Target: x + [1, 2, 3] +# Unit Target: x + [1, 2, 3] import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_plus_1_2_3_broadcasted.py b/benchmarks/x_plus_1_2_3_broadcasted.py index 36132d3d6..eb5e156ac 100644 --- a/benchmarks/x_plus_1_2_3_broadcasted.py +++ b/benchmarks/x_plus_1_2_3_broadcasted.py @@ -1,4 +1,4 @@ -# Target: x + [1, 2, 3] (Broadcasted) +# Unit Target: x + [1, 2, 3] (Broadcasted) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index d74ff023e..41b27b36a 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -1,4 +1,4 @@ -# Target: x + 42 +# Unit Target: x + 42 import random diff --git a/benchmarks/x_plus_42_tensor.py b/benchmarks/x_plus_42_tensor.py index a709b788e..9395926a1 100644 --- a/benchmarks/x_plus_42_tensor.py +++ b/benchmarks/x_plus_42_tensor.py @@ -1,4 +1,4 @@ -# Target: x + 42 (Tensor) +# Unit Target: x + 42 (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_plus_y.py b/benchmarks/x_plus_y.py index 39aae0377..bdf944d92 100644 --- a/benchmarks/x_plus_y.py +++ b/benchmarks/x_plus_y.py @@ -1,4 +1,4 @@ -# Target: x + y +# Unit Target: x + y import random diff --git a/benchmarks/x_plus_y_broadcasted_tensors.py b/benchmarks/x_plus_y_broadcasted_tensors.py index b2408d13f..e3960e895 100644 --- a/benchmarks/x_plus_y_broadcasted_tensors.py +++ b/benchmarks/x_plus_y_broadcasted_tensors.py @@ -1,4 +1,4 @@ -# Target: x + y (Broadcasted Tensors) +# Unit Target: x + y (Broadcasted Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_plus_y_tensor_and_scalar.py b/benchmarks/x_plus_y_tensor_and_scalar.py index 94224b449..8de52337c 100644 --- a/benchmarks/x_plus_y_tensor_and_scalar.py +++ b/benchmarks/x_plus_y_tensor_and_scalar.py @@ -1,4 +1,4 @@ -# Target: x + y (Tensor & Scalar) +# Unit Target: x + y (Tensor & Scalar) import random diff --git a/benchmarks/x_plus_y_tensors.py b/benchmarks/x_plus_y_tensors.py index 836d89c97..79693d82f 100644 --- a/benchmarks/x_plus_y_tensors.py +++ b/benchmarks/x_plus_y_tensors.py @@ -1,4 +1,4 @@ -# Target: x + y (Tensors) +# Unit Target: x + y (Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_times_1_2_3.py b/benchmarks/x_times_1_2_3.py index 040297c88..c6bd26c7d 100644 --- a/benchmarks/x_times_1_2_3.py +++ b/benchmarks/x_times_1_2_3.py @@ -1,4 +1,4 @@ -# Target: x * [1, 2, 3] +# Unit Target: x * [1, 2, 3] import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_times_1_2_3_broadcasted.py b/benchmarks/x_times_1_2_3_broadcasted.py index 77d56aa57..41e2b25f7 100644 --- a/benchmarks/x_times_1_2_3_broadcasted.py +++ b/benchmarks/x_times_1_2_3_broadcasted.py @@ -1,4 +1,4 @@ -# Target: x * [1, 2, 3] (Broadcasted) +# Unit Target: x * [1, 2, 3] (Broadcasted) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_times_7.py b/benchmarks/x_times_7.py index 156aafa1d..f82454750 100644 --- a/benchmarks/x_times_7.py +++ b/benchmarks/x_times_7.py @@ -1,4 +1,4 @@ -# Target: x * 7 +# Unit Target: x * 7 import random diff --git a/benchmarks/x_times_7_tensor.py b/benchmarks/x_times_7_tensor.py index c92d02031..f65cb6e92 100644 --- a/benchmarks/x_times_7_tensor.py +++ b/benchmarks/x_times_7_tensor.py @@ -1,4 +1,4 @@ -# Target: x * 7 (Tensor) +# Unit Target: x * 7 (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_times_y.py b/benchmarks/x_times_y.py index 50dc97d13..fd8092ffc 100644 --- a/benchmarks/x_times_y.py +++ b/benchmarks/x_times_y.py @@ -1,4 +1,4 @@ -# Target: x * y +# Unit Target: x * y import itertools import random diff --git a/benchmarks/x_times_y_broadcasted_tensors.py b/benchmarks/x_times_y_broadcasted_tensors.py index c830ea77c..18d50a74e 100644 --- a/benchmarks/x_times_y_broadcasted_tensors.py +++ b/benchmarks/x_times_y_broadcasted_tensors.py @@ -1,4 +1,4 @@ -# Target: x * y (Broadcasted Tensors) +# Unit Target: x * y (Broadcasted Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_times_y_tensor_and_scalar.py b/benchmarks/x_times_y_tensor_and_scalar.py index 7b7bad7e4..bd2cccaac 100644 --- a/benchmarks/x_times_y_tensor_and_scalar.py +++ b/benchmarks/x_times_y_tensor_and_scalar.py @@ -1,4 +1,4 @@ -# Target: x * y (Tensor & Scalar) +# Unit Target: x * y (Tensor & Scalar) import random diff --git a/benchmarks/x_times_y_tensors.py b/benchmarks/x_times_y_tensors.py index 7d561eb4a..adefc6b4b 100644 --- a/benchmarks/x_times_y_tensors.py +++ b/benchmarks/x_times_y_tensors.py @@ -1,4 +1,4 @@ -# Target: x * y (Tensors) +# Unit Target: x * y (Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION diff --git a/benchmarks/x_to_the_power_of_2.py b/benchmarks/x_to_the_power_of_2.py index 4391f6ad0..61a5f4577 100644 --- a/benchmarks/x_to_the_power_of_2.py +++ b/benchmarks/x_to_the_power_of_2.py @@ -1,4 +1,4 @@ -# Target: x**2 +# Unit Target: x**2 import random diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 231e0f087..069d688fb 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -284,15 +284,8 @@ def perform_measurements(path, script, target_id, metrics, samples, result): del result["targets"][target_id]["measurements"] -def main(): +def main(args): """Measurement script for the progress tracker""" - parser = argparse.ArgumentParser(description="Measurement script for the progress tracker") - - parser.add_argument("base", type=str, help="directory which contains the benchmarks") - parser.add_argument("--samples", type=int, default=30, help="number of samples to take") - parser.add_argument("--keep", action="store_true", help="flag to keep measurement scripts") - - args = parser.parse_args() base = pathlib.Path(args.base) samples = args.samples @@ -328,7 +321,15 @@ def main(): break # Check whether the script is a target or not - if not first_line.startswith("# Target:"): + if first_line.startswith("# Unit Target:"): + # Extract target name + target_name = first_line.replace("# Unit Target:", "").strip() + is_unit = True + elif first_line.startswith("# Full Target:"): + # Extract target name + target_name = first_line.replace("# Full Target:", "").strip() + is_unit = False + else: print() print(path) print("-" * len(str(path))) @@ -342,8 +343,7 @@ def main(): print() continue - # Extract target name and id - target_name = first_line.replace("# Target:", "").strip() + # Extract target id target_id = name_to_id(target_name) # Check whether the target is already registered @@ -371,6 +371,7 @@ def main(): "measurements": {}, "alerts": alerts, "code": "\n".join(lines), + "isUnit": is_unit, } # Perform and save measurements @@ -388,4 +389,10 @@ def main(): if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="Measurement script for the progress tracker") + + parser.add_argument("base", type=str, help="directory which contains the benchmarks") + parser.add_argument("--samples", type=int, default=30, help="number of samples to take") + parser.add_argument("--keep", action="store_true", help="flag to keep measurement scripts") + + main(parser.parse_args()) From 280ba7f8cde32025da294b3bf8ea4fb9087bffd8 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 12 Oct 2021 17:31:50 +0300 Subject: [PATCH 0408/1104] fix(benchmarks): use quantized cleartext loss instead of non quantized one in linear regression --- benchmarks/linear_regression.py | 40 ++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index 0612456d0..53105e796 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -85,6 +85,30 @@ def main(): def dequantize(self): return (self.values.astype(np.float32) + float(self.parameters.zp)) / self.parameters.q + def affine(self, w, b, min_y, max_y, n_y): + x_q = self.values + w_q = w.values + b_q = b.values + + q_x = self.parameters.q + q_w = w.parameters.q + q_b = b.parameters.q + + zp_x = self.parameters.zp + zp_w = w.parameters.zp + zp_b = b.parameters.zp + + q_y = (2 ** n_y - 1) / (max_y - min_y) + zp_y = int(round(min_y * q_y)) + + y_q = (q_y / (q_x * q_w)) * ( + (x_q + zp_x) @ (w_q + zp_w) + (q_x * q_w / q_b) * (b_q + zp_b) + ) + y_q -= min_y * q_y + y_q = y_q.round().clip(0, 2 ** n_y - 1).astype(np.uint) + + return QuantizedArray(y_q, QuantizationParameters(q_y, zp_y, n_y)) + class QuantizedFunction: def __init__(self, table): self.table = table @@ -163,7 +187,17 @@ def main(): for i, (x_i, y_i) in enumerate(zip(x_q, y)): x_i = [int(value) for value in x_i] - non_homomorphic_prediction = model.evaluate(x[i])[0] + non_homomorphic_prediction = ( + QuantizedArray(x_i, QuantizationParameters(q_x, zp_x, input_bits)) + .affine( + QuantizedArray.of(model.w, parameter_bits), + QuantizedArray.of(model.b, parameter_bits), + min_y, + max_y, + output_bits, + ) + .dequantize()[0] + ) # Measure: Evaluation Time (ms) homomorphic_prediction = QuantizedArray(engine.run(*x_i), y_parameters).dequantize() # Measure: End @@ -176,7 +210,7 @@ def main(): print(f"input = {x[i][0]}") print(f"output = {y_i:.4f}") - print(f"non homomorphic prediction = {non_homomorphic_loss:.4f}") + print(f"non homomorphic prediction = {non_homomorphic_prediction:.4f}") print(f"homomorphic prediction = {homomorphic_prediction:.4f}") non_homomorphic_loss /= len(y) @@ -191,7 +225,7 @@ def main(): # Measure: Non Homomorphic Loss = non_homomorphic_loss # Measure: Homomorphic Loss = homomorphic_loss # Measure: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference - # Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 20 + # Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 5 if __name__ == "__main__": From acdb80c6e3cd1e523aecbb4a71379092566c028a Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 12 Oct 2021 17:37:51 +0300 Subject: [PATCH 0409/1104] feat(benchmarks): add more metrics to logistic regression benchmark --- benchmarks/logistic_regression.py | 56 ++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index 79eeb3a0a..e1184d0de 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -1,5 +1,9 @@ # Full Target: Logistic Regression +# Disable line length warnings as we have a looooong metric... +# flake8: noqa: E501 +# pylint: disable=C0301 + import numpy as np import torch from common import BENCHMARK_CONFIGURATION @@ -76,6 +80,9 @@ def main(): self.zp = zp self.n = n + def __eq__(self, other): + return self.q == other.q and self.zp == other.zp and self.n == other.n + class QuantizedArray: def __init__(self, values, parameters): self.values = np.array(values) @@ -249,22 +256,53 @@ def main(): ) # Measure: End - correct = 0 - for x_i, y_i in zip(x_q, y): + non_homomorphic_correct = 0 + homomorphic_correct = 0 + + for i, (x_i, y_i) in enumerate(zip(x_q, y)): x_i = [int(value) for value in x_i] + non_homomorphic_prediction = round( + sigmoid.apply( + QuantizedArray(x_i, QuantizationParameters(q_x, zp_x, input_bits)).affine( + QuantizedArray.of(w, parameter_bits), + QuantizedArray.of(b, parameter_bits), + intermediate.min(), + intermediate.max(), + output_bits, + ) + ).dequantize()[0] + ) # Measure: Evaluation Time (ms) - prediction = round(QuantizedArray(engine.run(*x_i), y_parameters).dequantize()) + homomorphic_prediction = round(QuantizedArray(engine.run(*x_i), y_parameters).dequantize()) # Measure: End - if prediction == y_i: - correct += 1 + if non_homomorphic_prediction == y_i: + non_homomorphic_correct += 1 + if homomorphic_prediction == y_i: + homomorphic_correct += 1 - accuracy = (correct / len(y)) * 100 - print(f"Accuracy: {accuracy:.2f}%") + print() - # Measure: Accuracy (%) = accuracy - # Alert: Accuracy (%) < 85 + print(f"input = {x[i][0]}, {x[i][1]}") + print(f"output = {y_i:.4f}") + + print(f"non homomorphic prediction = {non_homomorphic_prediction:.4f}") + print(f"homomorphic prediction = {homomorphic_prediction:.4f}") + + non_homomorphic_accuracy = (non_homomorphic_correct / len(y)) * 100 + homomorphic_accuracy = (homomorphic_correct / len(y)) * 100 + difference = abs(homomorphic_accuracy - non_homomorphic_accuracy) + + print() + print(f"Non Homomorphic Accuracy: {non_homomorphic_accuracy:.4f}") + print(f"Homomorphic Accuracy: {homomorphic_accuracy:.4f}") + print(f"Difference Percentage: {difference:.2f}%") + + # Measure: Non Homomorphic Accuracy = non_homomorphic_accuracy + # Measure: Homomorphic Accuracy = homomorphic_accuracy + # Measure: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference + # Alert: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) > 5 if __name__ == "__main__": From 1c935f2d92245a0e8540ab7a6a870cc4fced2c70 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 12 Oct 2021 17:46:58 +0300 Subject: [PATCH 0410/1104] refactor(benchmarks): reduce alert tolerance from 5% to 2% from the cleartext version --- benchmarks/linear_regression.py | 2 +- benchmarks/logistic_regression.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index 53105e796..e0d15fb20 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -225,7 +225,7 @@ def main(): # Measure: Non Homomorphic Loss = non_homomorphic_loss # Measure: Homomorphic Loss = homomorphic_loss # Measure: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference - # Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 5 + # Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 2 if __name__ == "__main__": diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index e1184d0de..bd9f95a42 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -302,7 +302,7 @@ def main(): # Measure: Non Homomorphic Accuracy = non_homomorphic_accuracy # Measure: Homomorphic Accuracy = homomorphic_accuracy # Measure: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference - # Alert: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) > 5 + # Alert: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) > 2 if __name__ == "__main__": From 0cd33b6f67892eba22e5bbb7fc902872e34334e9 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 12 Oct 2021 17:32:15 +0200 Subject: [PATCH 0411/1104] feat(debugging): let's stop using custom_assert closes #637 --- .../bounds_measurement/inputset_eval.py | 4 ++-- concrete/common/common_helpers.py | 4 ++-- concrete/common/compilation/artifacts.py | 12 +++++----- concrete/common/data_types/dtypes_helpers.py | 16 +++++++------- concrete/common/data_types/floats.py | 4 ++-- concrete/common/data_types/integers.py | 4 ++-- concrete/common/debugging/__init__.py | 2 +- concrete/common/debugging/custom_assert.py | 8 +++---- concrete/common/debugging/drawing.py | 4 ++-- concrete/common/debugging/printing.py | 12 +++++----- concrete/common/mlir/converters.py | 22 +++++++++---------- concrete/common/mlir/mlir_converter.py | 4 ++-- concrete/common/operator_graph.py | 20 ++++++++--------- concrete/common/optimization/topological.py | 4 ++-- .../common/representation/intermediate.py | 14 ++++++------ concrete/common/tracing/base_tracer.py | 10 ++++----- concrete/common/tracing/tracing_helpers.py | 4 ++-- concrete/numpy/np_dtypes_helpers.py | 16 +++++++------- concrete/numpy/tracing.py | 22 +++++++++---------- 19 files changed, 92 insertions(+), 94 deletions(-) diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index 88051b441..904c0f009 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -8,7 +8,7 @@ from ..data_types.dtypes_helpers import ( get_base_value_for_python_constant_data, is_data_type_compatible_with, ) -from ..debugging import custom_assert +from ..debugging import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import IntermediateNode @@ -139,7 +139,7 @@ def eval_op_graph_bounds_on_inputset( """ def check_inputset_input_len_is_valid(data_to_check): - custom_assert( + assert_true( len(data_to_check) == len(op_graph.input_nodes), ( f"Got input data from inputset of len: {len(data_to_check)}, " diff --git a/concrete/common/common_helpers.py b/concrete/common/common_helpers.py index 53b3380c1..9ad5138d3 100644 --- a/concrete/common/common_helpers.py +++ b/concrete/common/common_helpers.py @@ -3,7 +3,7 @@ from typing import List, Optional from .data_types.integers import Integer -from .debugging import custom_assert +from .debugging import assert_true from .operator_graph import OPGraph from .representation.intermediate import IntermediateNode @@ -54,7 +54,7 @@ def check_op_graph_is_integer_program( """ offending_nodes = [] if offending_nodes_out is None else offending_nodes_out - custom_assert( + assert_true( isinstance(offending_nodes, list), f"offending_nodes_out must be a list, got {type(offending_nodes_out)}", ) diff --git a/concrete/common/compilation/artifacts.py b/concrete/common/compilation/artifacts.py index 474a4b9b8..9c7178b00 100644 --- a/concrete/common/compilation/artifacts.py +++ b/concrete/common/compilation/artifacts.py @@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, Optional, Union import networkx as nx from PIL import Image -from ..debugging import custom_assert, draw_graph, get_printable_graph +from ..debugging import assert_true, draw_graph, get_printable_graph from ..operator_graph import OPGraph from ..representation.intermediate import IntermediateNode from ..values import BaseValue @@ -102,7 +102,7 @@ class CompilationArtifacts: None """ - custom_assert(self.final_operation_graph is not None) + assert_true(self.final_operation_graph is not None) self.bounds_of_the_final_operation_graph = bounds def add_final_operation_graph_mlir(self, mlir: str): @@ -115,7 +115,7 @@ class CompilationArtifacts: None """ - custom_assert(self.final_operation_graph is not None) + assert_true(self.final_operation_graph is not None) self.mlir_of_the_final_operation_graph = mlir def export(self): @@ -188,7 +188,7 @@ class CompilationArtifacts: f.write(f"{representation}") if self.bounds_of_the_final_operation_graph is not None: - custom_assert(self.final_operation_graph is not None) + assert_true(self.final_operation_graph is not None) with open(output_directory.joinpath("bounds.txt"), "w", encoding="utf-8") as f: # TODO: # if nx.topological_sort is not deterministic between calls, @@ -196,11 +196,11 @@ class CompilationArtifacts: # thus, we may want to change this in the future for index, node in enumerate(nx.topological_sort(self.final_operation_graph.graph)): bounds = self.bounds_of_the_final_operation_graph.get(node) - custom_assert(bounds is not None) + assert_true(bounds is not None) f.write(f"%{index} :: [{bounds.get('min')}, {bounds.get('max')}]\n") if self.mlir_of_the_final_operation_graph is not None: - custom_assert(self.final_operation_graph is not None) + assert_true(self.final_operation_graph is not None) with open(output_directory.joinpath("mlir.txt"), "w", encoding="utf-8") as f: f.write(self.mlir_of_the_final_operation_graph) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 87cf75988..86eb7c24f 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -4,7 +4,7 @@ from copy import deepcopy from functools import partial from typing import Callable, Optional, Tuple, Union, cast -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..values import BaseValue, ClearTensor, EncryptedTensor, TensorValue from .base import BaseDataType from .floats import Float @@ -146,8 +146,8 @@ def find_type_to_hold_both_lossy( Returns: BaseDataType: The dtype able to hold (potentially lossy) dtype1 and dtype2 """ - custom_assert(isinstance(dtype1, BASE_DATA_TYPES), f"Unsupported dtype1: {type(dtype1)}") - custom_assert(isinstance(dtype2, BASE_DATA_TYPES), f"Unsupported dtype2: {type(dtype2)}") + assert_true(isinstance(dtype1, BASE_DATA_TYPES), f"Unsupported dtype1: {type(dtype1)}") + assert_true(isinstance(dtype2, BASE_DATA_TYPES), f"Unsupported dtype2: {type(dtype2)}") type_to_return: BaseDataType @@ -205,15 +205,15 @@ def mix_tensor_values_determine_holding_dtype( value2 dtypes. """ - custom_assert( + assert_true( isinstance(value1, TensorValue), f"Unsupported value1: {value1}, expected TensorValue" ) - custom_assert( + assert_true( isinstance(value2, TensorValue), f"Unsupported value2: {value2}, expected TensorValue" ) resulting_shape = broadcast_shapes(value1.shape, value2.shape) - custom_assert( + assert_true( resulting_shape is not None, ( f"Tensors have incompatible shapes which is not supported.\n" @@ -250,7 +250,7 @@ def mix_values_determine_holding_dtype(value1: BaseValue, value2: BaseValue) -> dtypes. """ - custom_assert( + assert_true( (value1.__class__ == value2.__class__), f"Cannot mix values of different types: value 1:{type(value1)}, value2: {type(value2)}", ) @@ -274,7 +274,7 @@ def get_base_data_type_for_python_constant_data(constant_data: Union[int, float] BaseDataType: The corresponding BaseDataType """ constant_data_type: BaseDataType - custom_assert( + assert_true( isinstance(constant_data, (int, float)), f"Unsupported constant data of type {type(constant_data)}", ) diff --git a/concrete/common/data_types/floats.py b/concrete/common/data_types/floats.py index a26c240b8..a537d28bb 100644 --- a/concrete/common/data_types/floats.py +++ b/concrete/common/data_types/floats.py @@ -2,7 +2,7 @@ from functools import partial -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from . import base @@ -15,7 +15,7 @@ class Float(base.BaseDataType): def __init__(self, bit_width: int) -> None: super().__init__() - custom_assert(bit_width in (32, 64), "Only 32 and 64 bits floats are supported") + assert_true(bit_width in (32, 64), "Only 32 and 64 bits floats are supported") self.bit_width = bit_width def __repr__(self) -> str: diff --git a/concrete/common/data_types/integers.py b/concrete/common/data_types/integers.py index 181a017b1..ed9654972 100644 --- a/concrete/common/data_types/integers.py +++ b/concrete/common/data_types/integers.py @@ -3,7 +3,7 @@ import math from typing import Any, Iterable -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from . import base @@ -15,7 +15,7 @@ class Integer(base.BaseDataType): def __init__(self, bit_width: int, is_signed: bool) -> None: super().__init__() - custom_assert(bit_width > 0, "bit_width must be > 0") + assert_true(bit_width > 0, "bit_width must be > 0") self.bit_width = bit_width self.is_signed = is_signed diff --git a/concrete/common/debugging/__init__.py b/concrete/common/debugging/__init__.py index c087039b5..811bf62da 100644 --- a/concrete/common/debugging/__init__.py +++ b/concrete/common/debugging/__init__.py @@ -1,4 +1,4 @@ """Module for debugging.""" -from .custom_assert import custom_assert +from .custom_assert import assert_true from .drawing import draw_graph from .printing import get_printable_graph diff --git a/concrete/common/debugging/custom_assert.py b/concrete/common/debugging/custom_assert.py index 71c88512f..1a639776c 100644 --- a/concrete/common/debugging/custom_assert.py +++ b/concrete/common/debugging/custom_assert.py @@ -1,7 +1,7 @@ """Provide some variants of assert.""" -def custom_assert(condition: bool, on_error_msg: str = "") -> None: +def _custom_assert(condition: bool, on_error_msg: str = "") -> None: """Provide a custom assert which is kept even if the optimized python mode is used. See https://docs.python.org/3/reference/simple_stmts.html#assert for the documentation @@ -25,7 +25,7 @@ def assert_true(condition: bool, on_error_msg: str = ""): on_error_msg(str): optional message for precising the error, in case of error """ - return custom_assert(condition, on_error_msg) + return _custom_assert(condition, on_error_msg) def assert_false(condition: bool, on_error_msg: str = ""): @@ -36,7 +36,7 @@ def assert_false(condition: bool, on_error_msg: str = ""): on_error_msg(str): optional message for precising the error, in case of error """ - return custom_assert(not condition, on_error_msg) + return _custom_assert(not condition, on_error_msg) def assert_not_reached(on_error_msg: str): @@ -46,4 +46,4 @@ def assert_not_reached(on_error_msg: str): on_error_msg(str): message for precising the error """ - return custom_assert(False, on_error_msg) + return _custom_assert(False, on_error_msg) diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index 65ce67628..372cbb79b 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -9,7 +9,7 @@ import matplotlib.pyplot as plt import networkx as nx from PIL import Image -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import ( ALL_IR_NODES, @@ -36,7 +36,7 @@ IR_NODE_COLOR_MAPPING = { } _missing_nodes_in_mapping = ALL_IR_NODES - IR_NODE_COLOR_MAPPING.keys() -custom_assert( +assert_true( len(_missing_nodes_in_mapping) == 0, ( f"Missing IR node in IR_NODE_COLOR_MAPPING : " diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 0b0444e7f..5ecfbc95e 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -4,7 +4,7 @@ from typing import Any, Dict import networkx as nx -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import Constant, Input, UnivariateFunction @@ -50,7 +50,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: Returns: str: a string to print or save in a file """ - custom_assert(isinstance(opgraph, OPGraph)) + assert_true(isinstance(opgraph, OPGraph)) list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) graph = opgraph.graph @@ -64,7 +64,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: # This code doesn't work with more than a single output. For more outputs, # we would need to change the way the destination are created: currently, # they only are done by incrementing i - custom_assert(len(node.outputs) == 1) + assert_true(len(node.outputs) == 1) if isinstance(node, Input): what_to_print = node.input_name @@ -91,9 +91,9 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: list_of_arg_name += [(index["input_idx"], str(map_table[pred]))] # Some checks, because the previous algorithm is not clear - custom_assert(len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name))) + assert_true(len(list_of_arg_name) == len(set(x[0] for x in list_of_arg_name))) list_of_arg_name.sort() - custom_assert([x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name)))) + assert_true([x[0] for x in list_of_arg_name] == list(range(len(list_of_arg_name)))) prefix_to_add_to_what_to_print = "" suffix_to_add_to_what_to_print = "" @@ -105,7 +105,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: if node.op_attributes["in_which_input_is_constant"] == 0: prefix_to_add_to_what_to_print = f"{shorten_a_constant(baked_constant)}, " else: - custom_assert( + assert_true( node.op_attributes["in_which_input_is_constant"] == 1, "'in_which_input_is_constant' should be a key of node.op_attributes", ) diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 255a6720a..21d09c791 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -21,15 +21,15 @@ from ..data_types.dtypes_helpers import ( value_is_encrypted_tensor_integer, ) from ..data_types.integers import Integer -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..representation.intermediate import Add, Constant, Dot, Mul, Sub, UnivariateFunction from ..values import TensorValue def add(node, preds, ir_to_mlir_node, ctx): """Convert an addition intermediate node.""" - custom_assert(len(node.inputs) == 2, "addition should have two inputs") - custom_assert(len(node.outputs) == 1, "addition should have a single output") + assert_true(len(node.inputs) == 2, "addition should have two inputs") + assert_true(len(node.outputs) == 1, "addition should have a single output") if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): @@ -72,8 +72,8 @@ def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): def sub(node, preds, ir_to_mlir_node, ctx): """Convert a subtraction intermediate node.""" - custom_assert(len(node.inputs) == 2, "subtraction should have two inputs") - custom_assert(len(node.outputs) == 1, "subtraction should have a single output") + assert_true(len(node.inputs) == 2, "subtraction should have two inputs") + assert_true(len(node.outputs) == 1, "subtraction should have a single output") if value_is_clear_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_unsigned_integer( node.inputs[1] ): @@ -96,8 +96,8 @@ def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): def mul(node, preds, ir_to_mlir_node, ctx): """Convert a multiplication intermediate node.""" - custom_assert(len(node.inputs) == 2, "multiplication should have two inputs") - custom_assert(len(node.outputs) == 1, "multiplication should have a single output") + assert_true(len(node.inputs) == 2, "multiplication should have two inputs") + assert_true(len(node.outputs) == 1, "multiplication should have a single output") if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): @@ -166,8 +166,8 @@ def constant(node, _, __, ctx): def apply_lut(node, preds, ir_to_mlir_node, ctx): """Convert a UnivariateFunction intermediate node.""" - custom_assert(len(node.inputs) == 1, "LUT should have a single input") - custom_assert(len(node.outputs) == 1, "LUT should have a single output") + assert_true(len(node.inputs) == 1, "LUT should have a single input") + assert_true(len(node.outputs) == 1, "LUT should have a single output") if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): raise TypeError("Only support LUT with encrypted unsigned integers inputs") if not value_is_encrypted_scalar_unsigned_integer(node.outputs[0]): @@ -192,8 +192,8 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): def dot(node, preds, ir_to_mlir_node, ctx): """Convert a dot intermediate node.""" - custom_assert(len(node.inputs) == 2, "Dot should have two inputs") - custom_assert(len(node.outputs) == 1, "Dot should have a single output") + assert_true(len(node.inputs) == 2, "Dot should have two inputs") + assert_true(len(node.outputs) == 1, "Dot should have a single output") if not ( ( value_is_encrypted_tensor_integer(node.inputs[0]) diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index 55af22122..984fc0fc3 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -17,7 +17,7 @@ from ..data_types.dtypes_helpers import ( value_is_encrypted_scalar_unsigned_integer, value_is_encrypted_tensor_unsigned_integer, ) -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import Input @@ -83,7 +83,7 @@ class MLIRConverter: if is_signed and not is_encrypted: # clear signed return IntegerType.get_signed(bit_width) # should be clear unsigned at this point - custom_assert(not is_signed and not is_encrypted) + assert_true(not is_signed and not is_encrypted) # unsigned integer are considered signless in the compiler return IntegerType.get_signless(bit_width) diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index ec3e06dc2..4c3498cb9 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -12,7 +12,7 @@ from .data_types.dtypes_helpers import ( ) from .data_types.floats import Float from .data_types.integers import Integer, make_integer_to_hold -from .debugging.custom_assert import custom_assert +from .debugging.custom_assert import assert_true from .representation.intermediate import Input, IntermediateNode from .tracing import BaseTracer from .tracing.tracing_helpers import create_graph_from_output_tracers @@ -31,14 +31,12 @@ class OPGraph: input_nodes: Dict[int, Input], output_nodes: Dict[int, IntermediateNode], ) -> None: - custom_assert( - len(input_nodes) > 0, "Got a graph without input nodes which is not supported" - ) - custom_assert( + assert_true(len(input_nodes) > 0, "Got a graph without input nodes which is not supported") + assert_true( all(isinstance(node, Input) for node in input_nodes.values()), "Got input nodes that were not Input, which is not supported", ) - custom_assert( + assert_true( all(isinstance(node, IntermediateNode) for node in output_nodes.values()), "Got output nodes which were not IntermediateNode, which is not supported", ) @@ -51,7 +49,7 @@ class OPGraph: def __call__(self, *args) -> Union[Any, Tuple[Any, ...]]: inputs = dict(enumerate(args)) - custom_assert( + assert_true( len(inputs) == len(self.input_nodes), f"Expected {len(self.input_nodes)} arguments, got {len(inputs)} : {args}", ) @@ -183,7 +181,7 @@ class OPGraph: min_data_type_constructor = get_type_constructor_for_constant_data(min_bound) max_data_type_constructor = get_type_constructor_for_constant_data(max_bound) - custom_assert( + assert_true( max_data_type_constructor == min_data_type_constructor, ( f"Got two different type constructors for min and max bound: " @@ -200,7 +198,7 @@ class OPGraph: (min_bound, max_bound), force_signed=False ) else: - custom_assert( + assert_true( isinstance(min_data_type, Float) and isinstance(max_data_type, Float), ( "min_bound and max_bound have different common types, " @@ -212,7 +210,7 @@ class OPGraph: output_value.dtype.underlying_type_constructor = data_type_constructor else: # Currently variable inputs are only allowed to be integers - custom_assert( + assert_true( isinstance(min_data_type, Integer) and isinstance(max_data_type, Integer), ( f"Inputs to a graph should be integers, got bounds that were float, \n" @@ -229,7 +227,7 @@ class OPGraph: # TODO: #57 manage multiple outputs from a node, probably requires an output_idx when # adding an edge - custom_assert(len(node.outputs) == 1) + assert_true(len(node.outputs) == 1) successors = self.graph.succ[node] for succ in successors: diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index b9941cc10..16d2d1df0 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -8,7 +8,7 @@ import networkx as nx from ..compilation.artifacts import CompilationArtifacts from ..data_types.floats import Float from ..data_types.integers import Integer -from ..debugging.custom_assert import assert_true, custom_assert +from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import Constant, Input, IntermediateNode, UnivariateFunction from ..values import TensorValue @@ -119,7 +119,7 @@ def convert_float_subgraph_to_fused_node( variable_input_nodes = [ node for node in float_subgraph_start_nodes if not isinstance(node, Constant) ] - custom_assert(len(variable_input_nodes) == 1) + assert_true(len(variable_input_nodes) == 1) current_subgraph_variable_input = variable_input_nodes[0] new_input_value = deepcopy(current_subgraph_variable_input.outputs[0]) diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index 2183a8597..144076ffc 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -12,7 +12,7 @@ from ..data_types.dtypes_helpers import ( mix_values_determine_holding_dtype, ) from ..data_types.integers import Integer -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..values import BaseValue, ClearScalar, EncryptedScalar, TensorValue IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" @@ -33,7 +33,7 @@ class IntermediateNode(ABC): **_kwargs, # This is to be able to feed arbitrary arguments to IntermediateNodes ) -> None: self.inputs = list(inputs) - custom_assert(all(isinstance(x, BaseValue) for x in self.inputs)) + assert_true(all(isinstance(x, BaseValue) for x in self.inputs)) # Register all IR nodes def __init_subclass__(cls, **kwargs): @@ -49,7 +49,7 @@ class IntermediateNode(ABC): """__init__ for a binary operation, ie two inputs.""" IntermediateNode.__init__(self, inputs) - custom_assert(len(self.inputs) == 2) + assert_true(len(self.inputs) == 2) self.outputs = [mix_values_func(self.inputs[0], self.inputs[1])] @@ -148,7 +148,7 @@ class Input(IntermediateNode): program_input_idx: int, ) -> None: super().__init__((input_value,)) - custom_assert(len(self.inputs) == 1) + assert_true(len(self.inputs) == 1) self.input_name = input_name self.program_input_idx = program_input_idx self.outputs = [deepcopy(self.inputs[0])] @@ -222,7 +222,7 @@ class UnivariateFunction(IntermediateNode): op_attributes: Optional[Dict[str, Any]] = None, ) -> None: super().__init__([input_base_value]) - custom_assert(len(self.inputs) == 1) + assert_true(len(self.inputs) == 1) self.arbitrary_func = arbitrary_func self.op_args = op_args if op_args is not None else () self.op_kwargs = op_kwargs if op_kwargs is not None else {} @@ -306,9 +306,9 @@ class Dot(IntermediateNode): ] = default_dot_evaluation_function, ) -> None: super().__init__(inputs) - custom_assert(len(self.inputs) == 2) + assert_true(len(self.inputs) == 2) - custom_assert( + assert_true( all( isinstance(input_value, TensorValue) and input_value.ndim == 1 for input_value in self.inputs diff --git a/concrete/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py index 1bd9ad747..086b24139 100644 --- a/concrete/common/tracing/base_tracer.py +++ b/concrete/common/tracing/base_tracer.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Iterable, List, Tuple, Type, Union -from ..debugging.custom_assert import custom_assert +from ..debugging.custom_assert import assert_true from ..representation.intermediate import ( IR_MIX_VALUES_FUNC_ARG_NAME, Add, @@ -111,7 +111,7 @@ class BaseTracer(ABC): Add, ) - custom_assert(len(result_tracer) == 1) + assert_true(len(result_tracer) == 1) return result_tracer[0] # With that is that x + 1 and 1 + x have the same graph. If we want to keep @@ -128,7 +128,7 @@ class BaseTracer(ABC): Sub, ) - custom_assert(len(result_tracer) == 1) + assert_true(len(result_tracer) == 1) return result_tracer[0] def __rsub__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": @@ -140,7 +140,7 @@ class BaseTracer(ABC): Sub, ) - custom_assert(len(result_tracer) == 1) + assert_true(len(result_tracer) == 1) return result_tracer[0] def __mul__(self, other: Union["BaseTracer", Any]) -> "BaseTracer": @@ -152,7 +152,7 @@ class BaseTracer(ABC): Mul, ) - custom_assert(len(result_tracer) == 1) + assert_true(len(result_tracer) == 1) return result_tracer[0] # With that is that x * 3 and 3 * x have the same graph. If we want to keep diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index 2e3eea6a8..24b2f673f 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -6,7 +6,7 @@ from typing import Callable, Dict, Iterable, OrderedDict, Set, Type import networkx as nx from networkx.algorithms.dag import is_directed_acyclic_graph -from ..debugging.custom_assert import assert_true, custom_assert +from ..debugging.custom_assert import assert_true from ..representation.intermediate import Input from ..values import BaseValue from .base_tracer import BaseTracer @@ -124,7 +124,7 @@ def create_graph_from_output_tracers( current_tracers = next_tracers - custom_assert(is_directed_acyclic_graph(graph)) + assert_true(is_directed_acyclic_graph(graph)) # Check each edge is unique unique_edges = set( diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index dc5a0d3bf..599886068 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -17,7 +17,7 @@ from ..common.data_types.dtypes_helpers import ( ) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer -from ..common.debugging.custom_assert import custom_assert +from ..common.debugging.custom_assert import assert_true from ..common.values import BaseValue, TensorValue NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { @@ -72,13 +72,13 @@ def convert_base_data_type_to_numpy_dtype(common_dtype: BaseDataType) -> numpy.d Returns: numpy.dtype: The resulting numpy.dtype """ - custom_assert( + assert_true( isinstance(common_dtype, BASE_DATA_TYPES), f"Unsupported common_dtype: {type(common_dtype)}" ) type_to_return: numpy.dtype if isinstance(common_dtype, Float): - custom_assert( + assert_true( common_dtype.bit_width in ( 32, @@ -117,7 +117,7 @@ def get_base_data_type_for_numpy_or_python_constant_data(constant_data: Any) -> BaseDataType: The corresponding BaseDataType """ base_dtype: BaseDataType - custom_assert( + assert_true( isinstance( constant_data, (int, float, list, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES) ), @@ -159,12 +159,12 @@ def get_base_value_for_numpy_or_python_constant_data( with `encrypted` as keyword argument (forwarded to the BaseValue `__init__` method). """ constant_data_value: Callable[..., BaseValue] - custom_assert( + assert_true( not isinstance(constant_data, list), "Unsupported constant data of type list " "(if you meant to use a list as an array, please use numpy.array instead)", ) - custom_assert( + assert_true( isinstance( constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES), @@ -198,7 +198,7 @@ def get_numpy_function_output_dtype( List[numpy.dtype]: The ordered numpy dtypes of the function outputs """ if isinstance(function, numpy.ufunc): - custom_assert( + assert_true( (len(input_dtypes) == function.nin), f"Expected {function.nin} types, got {len(input_dtypes)}: {input_dtypes}", ) @@ -231,7 +231,7 @@ def get_type_constructor_for_numpy_or_python_constant_data(constant_data: Any): constant_data (Any): The data for which we want to determine the type constructor. """ - custom_assert( + assert_true( isinstance(constant_data, (int, float, numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)), f"Unsupported constant data of type {type(constant_data)}", ) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index c9b5c6966..12ffb5f27 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -7,7 +7,7 @@ import numpy from numpy.typing import DTypeLike from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype -from ..common.debugging.custom_assert import assert_true, custom_assert +from ..common.debugging.custom_assert import assert_true from ..common.operator_graph import OPGraph from ..common.representation.intermediate import Constant, Dot, UnivariateFunction from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters @@ -41,7 +41,7 @@ class NPTracer(BaseTracer): """ if method == "__call__": tracing_func = self.get_tracing_func_for_np_function(ufunc) - custom_assert( + assert_true( (len(kwargs) == 0), f"**kwargs are currently not supported for numpy ufuncs, ufunc: {ufunc.__name__}", ) @@ -58,7 +58,7 @@ class NPTracer(BaseTracer): Read more: https://numpy.org/doc/stable/user/basics.dispatch.html#basics-dispatch """ tracing_func = self.get_tracing_func_for_np_function(func) - custom_assert( + assert_true( (len(kwargs) == 0), f"**kwargs are currently not supported for numpy functions, func: {func}", ) @@ -77,10 +77,10 @@ class NPTracer(BaseTracer): Returns: NPTracer: The NPTracer representing the casting operation """ - custom_assert( + assert_true( len(args) == 0, f"astype currently only supports tracing without *args, got {args}" ) - custom_assert( + assert_true( (len(kwargs) == 0), f"astype currently only supports tracing without **kwargs, got {kwargs}", ) @@ -150,9 +150,9 @@ class NPTracer(BaseTracer): Returns: NPTracer: The output NPTracer containing the traced function """ - custom_assert(len(input_tracers) == 1) + assert_true(len(input_tracers) == 1) common_output_dtypes = cls._manage_dtypes(unary_operator, *input_tracers) - custom_assert(len(common_output_dtypes) == 1) + assert_true(len(common_output_dtypes) == 1) traced_computation = UnivariateFunction( input_base_value=input_tracers[0].output, @@ -179,7 +179,7 @@ class NPTracer(BaseTracer): Returns: NPTracer: The output NPTracer containing the traced function """ - custom_assert(len(input_tracers) == 2) + assert_true(len(input_tracers) == 2) # One of the inputs has to be constant if isinstance(input_tracers[0].traced_computation, Constant): @@ -204,7 +204,7 @@ class NPTracer(BaseTracer): return binary_operator(x, baked_constant, **kwargs) common_output_dtypes = cls._manage_dtypes(binary_operator, *input_tracers) - custom_assert(len(common_output_dtypes) == 1) + assert_true(len(common_output_dtypes) == 1) op_kwargs = deepcopy(kwargs) op_kwargs["baked_constant"] = baked_constant @@ -242,7 +242,7 @@ class NPTracer(BaseTracer): assert_true((num_args := len(args)) == 2, f"dot expects 2 inputs got {num_args}") common_output_dtypes = self._manage_dtypes(numpy.dot, *args) - custom_assert(len(common_output_dtypes) == 1) + assert_true(len(common_output_dtypes) == 1) traced_computation = Dot( [input_tracer.output for input_tracer in args], @@ -399,7 +399,7 @@ list_of_not_supported = [ if ufunc.nin not in [1, 2] ] -custom_assert(len(list_of_not_supported) == 0, f"Not supported nin's, {list_of_not_supported}") +assert_true(len(list_of_not_supported) == 0, f"Not supported nin's, {list_of_not_supported}") del list_of_not_supported # We are adding initial support for `np.array(...)` +,-,* `BaseTracer` From a4da3b82101f53a8423161bf309e58908dbb8b5a Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 12 Oct 2021 11:35:52 +0200 Subject: [PATCH 0412/1104] feat(tracing): add output_idx information in edges - renamed output_index to output_idx in BaseTracer - update tracing and fusing code to manage output_idx correctly - update OPGraph evaluate and update_values_with_bounds to manage output_idx - update tests checking graph validity to have output_idx set properly - the support of actual multi-output nodes is in #81 --- concrete/common/debugging/drawing.py | 1 + concrete/common/debugging/printing.py | 1 + concrete/common/extensions/table.py | 2 +- concrete/common/operator_graph.py | 41 +++++++++++++++++---- concrete/common/optimization/topological.py | 16 ++++++-- concrete/common/tracing/base_tracer.py | 10 +++-- concrete/common/tracing/tracing_helpers.py | 8 +++- concrete/numpy/tracing.py | 10 ++--- tests/common/extensions/test_table.py | 8 ++-- tests/conftest.py | 2 +- tests/helpers/test_conftest.py | 16 ++++---- tests/numpy/test_tracing.py | 8 ++-- 12 files changed, 84 insertions(+), 39 deletions(-) diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index 372cbb79b..f9f1e02f3 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -89,6 +89,7 @@ def draw_graph( } nx.set_node_attributes(graph, attributes) + # TODO: #639 adapt drawing routine to manage output_idx for edge in graph.edges(keys=True): idx = graph.edges[edge]["input_idx"] graph.edges[edge]["label"] = f" {idx} " # spaces are there intentionally for a better look diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 5ecfbc95e..688a4faef 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -61,6 +61,7 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: for node in nx.topological_sort(graph): + # TODO: #640 # This code doesn't work with more than a single output. For more outputs, # we would need to change the way the destination are created: currently, # they only are done by incrementing i diff --git a/concrete/common/extensions/table.py b/concrete/common/extensions/table.py index 8e882bd52..0455d3cc1 100644 --- a/concrete/common/extensions/table.py +++ b/concrete/common/extensions/table.py @@ -45,7 +45,7 @@ class LookupTable: return key.__class__( inputs=[key], traced_computation=traced_computation, - output_index=0, + output_idx=0, ) # if not, it means table is indexed with a constant diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index 4c3498cb9..92f338984 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -126,13 +126,44 @@ class OPGraph: """ node_results: Dict[IntermediateNode, Any] = {} + def get_result_of_node_at_index(node: IntermediateNode, output_idx: int) -> Any: + """Get the output result at index output_idx for a node. + + Args: + node (IntermediateNode): the node from which we want the output. + output_idx (int): which output we want. + + Returns: + Any: the output value of the evaluation of node. + """ + result = node_results[node] + # TODO: #81 remove no cover once we have nodes with multiple outputs + if isinstance(result, tuple): # pragma: no cover + # If the node has multiple outputs (i.e. the result is a tuple), return the + # requested output + return result[output_idx] + # If the result is not a tuple, then the result is the node's only output. Check that + # the requested index is 0 (as it's the only valid value) and return the result itself. + assert_true( + output_idx == 0, + f"Unable to get output at index {output_idx} for node {node}.\n" + f"Node result: {result}", + ) + return result + for node in nx.topological_sort(self.graph): if not isinstance(node, Input): curr_inputs = {} for pred_node in self.graph.pred[node]: edges = self.graph.get_edge_data(pred_node, node) curr_inputs.update( - {edge["input_idx"]: node_results[pred_node] for edge in edges.values()} + { + edge["input_idx"]: get_result_of_node_at_index( + pred_node, + output_idx=edge["output_idx"], + ) + for edge in edges.values() + } ) node_results[node] = node.evaluate(curr_inputs) else: @@ -225,16 +256,12 @@ class OPGraph: node.outputs[0] = deepcopy(node.inputs[0]) - # TODO: #57 manage multiple outputs from a node, probably requires an output_idx when - # adding an edge - assert_true(len(node.outputs) == 1) - successors = self.graph.succ[node] for succ in successors: edge_data = self.graph.get_edge_data(node, succ) for edge in edge_data.values(): - input_idx = edge["input_idx"] - succ.inputs[input_idx] = deepcopy(node.outputs[0]) + input_idx, output_idx = edge["input_idx"], edge["output_idx"] + succ.inputs[input_idx] = deepcopy(node.outputs[output_idx]) def prune_nodes(self): """Remove unreachable nodes from outputs.""" diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index 16d2d1df0..ca6cdb012 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -73,10 +73,14 @@ def fuse_float_operations( succ_edge_data = deepcopy(nx_graph.get_edge_data(terminal_node, succ)) for edge_key, edge_data in succ_edge_data.items(): nx_graph.remove_edge(terminal_node, succ, key=edge_key) - nx_graph.add_edge(fused_node, succ, key=edge_key, **edge_data) + # fused_node is always a UnivariateFunction so output_idx == 0 always + new_edge_data = deepcopy(edge_data) + new_edge_data["output_idx"] = 0 + nx_graph.add_edge(fused_node, succ, key=edge_key, **new_edge_data) # Connect the node feeding the subgraph contained in fused_node - nx_graph.add_edge(node_before_subgraph, fused_node, input_idx=0) + # node_before_subgraph has a single integer output currently so output_idx == 0 + nx_graph.add_edge(node_before_subgraph, fused_node, input_idx=0, output_idx=0) op_graph.prune_nodes() if compilation_artifacts is not None: @@ -122,6 +126,7 @@ def convert_float_subgraph_to_fused_node( assert_true(len(variable_input_nodes) == 1) current_subgraph_variable_input = variable_input_nodes[0] + assert_true(len(current_subgraph_variable_input.outputs) == 1) new_input_value = deepcopy(current_subgraph_variable_input.outputs[0]) nx_graph = op_graph.graph @@ -147,11 +152,14 @@ def convert_float_subgraph_to_fused_node( float_subgraph.remove_edge( current_subgraph_variable_input, node_after_input, key=edge_key ) + # new_subgraph_variable_input is always an Input so output_idx == 0 always + new_edge_data = deepcopy(edge_data) + new_edge_data["output_idx"] = 0 float_subgraph.add_edge( new_subgraph_variable_input, node_after_input, key=edge_key, - **edge_data, + **new_edge_data, ) float_op_subgraph = OPGraph.from_graph( @@ -160,6 +168,8 @@ def convert_float_subgraph_to_fused_node( [terminal_node], ) + assert_true(len(terminal_node.outputs) == 1) + # Create fused_node fused_node = UnivariateFunction( deepcopy(new_subgraph_variable_input.inputs[0]), diff --git a/concrete/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py index 086b24139..4a6f450e4 100644 --- a/concrete/common/tracing/base_tracer.py +++ b/concrete/common/tracing/base_tracer.py @@ -19,6 +19,7 @@ class BaseTracer(ABC): inputs: List["BaseTracer"] traced_computation: IntermediateNode + output_idx: int output: BaseValue _mix_values_func: Callable[..., BaseValue] @@ -26,11 +27,12 @@ class BaseTracer(ABC): self, inputs: Iterable["BaseTracer"], traced_computation: IntermediateNode, - output_index: int, + output_idx: int, ) -> None: self.inputs = list(inputs) self.traced_computation = traced_computation - self.output = traced_computation.outputs[output_index] + self.output_idx = output_idx + self.output = traced_computation.outputs[output_idx] @abstractmethod def _supports_other_operand(self, other: Any) -> bool: @@ -96,8 +98,8 @@ class BaseTracer(ABC): ) output_tracers = tuple( - self.__class__(sanitized_inputs, traced_computation, output_index) - for output_index in range(len(traced_computation.outputs)) + self.__class__(sanitized_inputs, traced_computation, output_idx) + for output_idx in range(len(traced_computation.outputs)) ) return output_tracers diff --git a/concrete/common/tracing/tracing_helpers.py b/concrete/common/tracing/tracing_helpers.py index 24b2f673f..8d114ed35 100644 --- a/concrete/common/tracing/tracing_helpers.py +++ b/concrete/common/tracing/tracing_helpers.py @@ -115,8 +115,14 @@ def create_graph_from_output_tracers( for input_idx, input_tracer in enumerate(tracer.inputs): input_ir_node = input_tracer.traced_computation + output_idx = input_tracer.output_idx graph.add_node(input_ir_node) - graph.add_edge(input_ir_node, current_ir_node, input_idx=input_idx) + graph.add_edge( + input_ir_node, + current_ir_node, + input_idx=input_idx, + output_idx=output_idx, + ) if input_tracer not in visited_tracers: next_tracers.update({input_tracer: None}) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 12ffb5f27..00e1edfa6 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -93,9 +93,7 @@ class NPTracer(BaseTracer): output_dtype=output_dtype, op_name=f"astype({normalized_numpy_dtype})", ) - output_tracer = self.__class__( - [self], traced_computation=traced_computation, output_index=0 - ) + output_tracer = self.__class__([self], traced_computation=traced_computation, output_idx=0) return output_tracer @staticmethod @@ -164,7 +162,7 @@ class NPTracer(BaseTracer): output_tracer = cls( input_tracers, traced_computation=traced_computation, - output_index=0, + output_idx=0, ) return output_tracer @@ -229,7 +227,7 @@ class NPTracer(BaseTracer): output_tracer = cls( (input_tracers[in_which_input_is_variable],), traced_computation=traced_computation, - output_index=0, + output_idx=0, ) return output_tracer @@ -253,7 +251,7 @@ class NPTracer(BaseTracer): output_tracer = self.__class__( args, traced_computation=traced_computation, - output_index=0, + output_idx=0, ) return output_tracer diff --git a/tests/common/extensions/test_table.py b/tests/common/extensions/test_table.py index d6a95999a..ac90049a1 100644 --- a/tests/common/extensions/test_table.py +++ b/tests/common/extensions/test_table.py @@ -66,7 +66,7 @@ def test_lookup_table_encrypted_lookup(test_helpers): # pylint: enable=protected-access ref_graph.add_node(output_arbitrary_function) - ref_graph.add_edge(input_x, output_arbitrary_function, input_idx=0) + ref_graph.add_edge(input_x, output_arbitrary_function, input_idx=0, output_idx=0) # TODO: discuss if this check is enough as == is not overloaded properly for UnivariateFunction assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) @@ -112,10 +112,10 @@ def test_lookup_table_encrypted_and_plain_lookup(test_helpers): output_add = ir.Add((intermediate_arbitrary_function.outputs[0], constant_3.outputs[0])) ref_graph.add_node(output_add) - ref_graph.add_edge(input_x, intermediate_arbitrary_function, input_idx=0) + ref_graph.add_edge(input_x, intermediate_arbitrary_function, input_idx=0, output_idx=0) - ref_graph.add_edge(intermediate_arbitrary_function, output_add, input_idx=0) - ref_graph.add_edge(constant_3, output_add, input_idx=1) + ref_graph.add_edge(intermediate_arbitrary_function, output_add, input_idx=0, output_idx=0) + ref_graph.add_edge(constant_3, output_add, input_idx=1, output_idx=0) # TODO: discuss if this check is enough as == is not overloaded properly for UnivariateFunction assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) diff --git a/tests/conftest.py b/tests/conftest.py index 73acdbb9c..d3e38870b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -194,7 +194,7 @@ class TestHelpers: def digraphs_are_equivalent(reference: nx.MultiDiGraph, to_compare: nx.MultiDiGraph): """Check that two digraphs are equivalent without modifications""" # edge_match is a copy of node_match - edge_matcher = iso.categorical_multiedge_match("input_idx", None) + edge_matcher = iso.categorical_multiedge_match(["input_idx", "output_idx"], [None, None]) node_matcher = iso.generic_node_match( "_test_content", None, TestHelpers.nodes_are_equivalent ) diff --git a/tests/helpers/test_conftest.py b/tests/helpers/test_conftest.py index 9f5a50a29..65a5d3a09 100644 --- a/tests/helpers/test_conftest.py +++ b/tests/helpers/test_conftest.py @@ -28,27 +28,27 @@ def test_digraphs_are_equivalent(test_helpers): t_1 = TestNode("Mul") t_2 = TestNode("TLU") - g_1.add_edge(t_0, t_2, input_idx=0) - g_1.add_edge(t_1, t_2, input_idx=1) + g_1.add_edge(t_0, t_2, input_idx=0, output_idx=0) + g_1.add_edge(t_1, t_2, input_idx=1, output_idx=0) t0p = TestNode("Add") t1p = TestNode("Mul") t2p = TestNode("TLU") - g_2.add_edge(t1p, t2p, input_idx=1) - g_2.add_edge(t0p, t2p, input_idx=0) + g_2.add_edge(t1p, t2p, input_idx=1, output_idx=0) + g_2.add_edge(t0p, t2p, input_idx=0, output_idx=0) bad_g2 = nx.MultiDiGraph() bad_t0 = TestNode("Not Add") - bad_g2.add_edge(bad_t0, t_2, input_idx=0) - bad_g2.add_edge(t_1, t_2, input_idx=1) + bad_g2.add_edge(bad_t0, t_2, input_idx=0, output_idx=0) + bad_g2.add_edge(t_1, t_2, input_idx=1, output_idx=0) bad_g3 = nx.MultiDiGraph() - bad_g3.add_edge(t_0, t_2, input_idx=1) - bad_g3.add_edge(t_1, t_2, input_idx=0) + bad_g3.add_edge(t_0, t_2, input_idx=1, output_idx=0) + bad_g3.add_edge(t_1, t_2, input_idx=0, output_idx=0) assert test_helpers.digraphs_are_equivalent(g_1, g_2), "Graphs should be equivalent" assert not test_helpers.digraphs_are_equivalent(g_1, bad_g2), "Graphs should not be equivalent" diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index c7fa35a3e..7c953a44c 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -159,11 +159,11 @@ def test_numpy_tracing_binary_op(operation, x, y, test_helpers): ref_graph.add_node(add_node_z) ref_graph.add_node(returned_final_node) - ref_graph.add_edge(input_x, add_node_z, input_idx=0) - ref_graph.add_edge(input_x, add_node_z, input_idx=1) + ref_graph.add_edge(input_x, add_node_z, input_idx=0, output_idx=0) + ref_graph.add_edge(input_x, add_node_z, input_idx=1, output_idx=0) - ref_graph.add_edge(add_node_z, returned_final_node, input_idx=0) - ref_graph.add_edge(input_y, returned_final_node, input_idx=1) + ref_graph.add_edge(add_node_z, returned_final_node, input_idx=0, output_idx=0) + ref_graph.add_edge(input_y, returned_final_node, input_idx=1, output_idx=0) assert test_helpers.digraphs_are_equivalent(ref_graph, op_graph.graph) From 17704da169cd08cfeea6fccc0c17e43ccbfcebf2 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 13 Oct 2021 11:03:14 +0200 Subject: [PATCH 0413/1104] test: make our tests reproducible by seeding random generators in python random or in numpy random closes #546 --- Makefile | 3 ++ poetry.lock | 128 +++++++++++++++++++++++-------------------------- pyproject.toml | 1 + 3 files changed, 65 insertions(+), 67 deletions(-) diff --git a/Makefile b/Makefile index b5d680a3c..ddd3d4aa2 100644 --- a/Makefile +++ b/Makefile @@ -76,10 +76,13 @@ PCC_DEPS += check_version_coherence pcc_internal: $(PCC_DEPS) .PHONY: pcc_internal +# One can reproduce pytest thanks to the --randomly-seed which is given by +# pytest-randomly pytest: poetry run pytest -svv \ --global-coverage-infos-json=global-coverage-infos.json \ --cov=$(SRC_DIR) --cov-fail-under=100 \ + --randomly-dont-reorganize \ --cov-report=term-missing:skip-covered tests/ .PHONY: pytest diff --git a/poetry.lock b/poetry.lock index a7a3574a3..9d9fa4e6c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -47,7 +47,7 @@ wrapt = ">=1.11,<1.13" name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -55,7 +55,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -191,7 +191,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0" +version = "6.0.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -373,7 +373,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "4.8.1" description = "Read metadata from Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -401,7 +401,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -1008,7 +1008,7 @@ python-versions = ">=3.7,<3.11" name = "packaging" version = "21.0" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1097,7 +1097,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1150,7 +1150,7 @@ python-versions = "*" name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1268,7 +1268,7 @@ python-versions = ">=3.6" name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1300,6 +1300,18 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-randomly" +version = "3.10.1" +description = "Pytest plugin to randomly order tests and control random.seed." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +pytest = "*" + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1745,7 +1757,7 @@ test = ["pytest", "pathlib2"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -1890,7 +1902,7 @@ python-versions = "*" name = "zipp" version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1901,7 +1913,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "b044859111d093c4bc499eed965c77062ebddab73f46f8b704edc801868696f5" +content-hash = "ca03765d05748d41d638bffcb0a7fb021b91636884eeb3d76bcd86f0e29a1511" [metadata.files] alabaster = [ @@ -2025,41 +2037,39 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:3dfb23cc180b674a11a559183dff9655beb9da03088f3fe3c4f3a6d200c86f05"}, - {file = "coverage-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5dd5ae0a9cd55d71f1335c331e9625382239b8cede818fb62d8d2702336dbf8"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8426fec5ad5a6e8217921716b504e9b6e1166dc147e8443b4855e329db686282"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aa5d4d43fa18cc9d0c6e02a83de0b9729b5451a9066574bd276481474f0a53ab"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78dd3eeb8f5ff26d2113c41836bac04a9ea91be54c346826b54a373133c8c53"}, - {file = "coverage-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:581fddd2f883379bd5af51da9233e0396b6519f3d3eeae4fb88867473be6d56e"}, - {file = "coverage-6.0-cp310-cp310-win32.whl", hash = "sha256:43bada49697a62ffa0283c7f01bbc76aac562c37d4bb6c45d56dd008d841194e"}, - {file = "coverage-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:fa816e97cfe1f691423078dffa39a18106c176f28008db017b3ce3e947c34aa5"}, - {file = "coverage-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5c191e01b23e760338f19d8ba2470c0dad44c8b45e41ac043b2db84efc62f695"}, - {file = "coverage-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274a612f67f931307706b60700f1e4cf80e1d79dff6c282fc9301e4565e78724"}, - {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9dbfcbc56d8de5580483cf2caff6a59c64d3e88836cbe5fb5c20c05c29a8808"}, - {file = "coverage-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e63490e8a6675cee7a71393ee074586f7eeaf0e9341afd006c5d6f7eec7c16d7"}, - {file = "coverage-6.0-cp36-cp36m-win32.whl", hash = "sha256:72f8c99f1527c5a8ee77c890ea810e26b39fd0b4c2dffc062e20a05b2cca60ef"}, - {file = "coverage-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:88f1810eb942e7063d051d87aaaa113eb5fd5a7fd2cda03a972de57695b8bb1a"}, - {file = "coverage-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:befb5ffa9faabef6dadc42622c73de168001425258f0b7e402a2934574e7a04b"}, - {file = "coverage-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dbda34e8e26bd86606ba8a9c13ccb114802e01758a3d0a75652ffc59a573220"}, - {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4ee5815c776dfa3958ba71c7cd4cdd8eb40d79358a18352feb19562fe4408c4"}, - {file = "coverage-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d82cbef1220703ce56822be7fbddb40736fc1a928ac893472df8aff7421ae0aa"}, - {file = "coverage-6.0-cp37-cp37m-win32.whl", hash = "sha256:d795a2c92fe8cb31f6e9cd627ee4f39b64eb66bf47d89d8fcf7cb3d17031c887"}, - {file = "coverage-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6e216e4021c934246c308fd3e0d739d9fa8a3f4ea414f584ab90ef9c1592f282"}, - {file = "coverage-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8305e14112efb74d0b5fec4df6e41cafde615c2392a7e51c84013cafe945842c"}, - {file = "coverage-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4865dc4a7a566147cbdc2b2f033a6cccc99a7dcc89995137765c384f6c73110b"}, - {file = "coverage-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:25df2bc53a954ba2ccf230fa274d1de341f6aa633d857d75e5731365f7181749"}, - {file = "coverage-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08fd55d2e00dac4c18a2fa26281076035ec86e764acdc198b9185ce749ada58f"}, - {file = "coverage-6.0-cp38-cp38-win32.whl", hash = "sha256:11ce082eb0f7c2bbfe96f6c8bcc3a339daac57de4dc0f3186069ec5c58da911c"}, - {file = "coverage-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7844a8c6a0fee401edbf578713c2473e020759267c40261b294036f9d3eb6a2d"}, - {file = "coverage-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bea681309bdd88dd1283a8ba834632c43da376d9bce05820826090aad80c0126"}, - {file = "coverage-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e735ab8547d8a1fe8e58dd765d6f27ac539b395f52160d767b7189f379f9be7a"}, - {file = "coverage-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7593a49300489d064ebb6c58539f52cbbc4a2e6a4385de5e92cae1563f88a425"}, - {file = "coverage-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adb0f4c3c8ba8104378518a1954cbf3d891a22c13fd0e0bf135391835f44f288"}, - {file = "coverage-6.0-cp39-cp39-win32.whl", hash = "sha256:8da0c4a26a831b392deaba5fdd0cd7838d173b47ce2ec3d0f37be630cb09ef6e"}, - {file = "coverage-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7af2f8e7bb54ace984de790e897f858e88068d8fbc46c9490b7c19c59cf51822"}, - {file = "coverage-6.0-pp36-none-any.whl", hash = "sha256:82b58d37c47d93a171be9b5744bcc96a0012cbf53d5622b29a49e6be2097edd7"}, - {file = "coverage-6.0-pp37-none-any.whl", hash = "sha256:fff04bfefb879edcf616f1ce5ea6f4a693b5976bdc5e163f8464f349c25b59f0"}, - {file = "coverage-6.0.tar.gz", hash = "sha256:17983f6ccc47f4864fd16d20ff677782b23d1207bf222d10e4d676e4636b0872"}, + {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, + {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, + {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, + {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, + {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, + {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, + {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, + {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, + {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, + {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, + {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, + {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, + {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, + {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, + {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, + {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, + {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, + {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, + {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, + {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, + {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, + {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, + {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, ] cryptography = [ {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, @@ -2322,22 +2332,12 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2346,21 +2346,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2370,9 +2363,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2739,6 +2729,10 @@ pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] +pytest-randomly = [ + {file = "pytest-randomly-3.10.1.tar.gz", hash = "sha256:d4ef5dbf27e542e6a4e4cec7a20ef3f1b906bce21fa340ca5657b5326ef23a64"}, + {file = "pytest_randomly-3.10.1-py3-none-any.whl", hash = "sha256:d28d490e3a743bdd64c5bc87c5fc182eac966ba6432c6bb6b224e32e76527e9e"}, +] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, diff --git a/pyproject.toml b/pyproject.toml index 4056d65cc..18435aa1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ numpy = "^1.21.2" pygraphviz = "^1.7" Pillow = "^8.3.2" loguru = "^0.5.3" +pytest-randomly = "^3.10.1" [tool.poetry.dev-dependencies] isort = "^5.9.3" From 6e79c0baf5544cb390ab97608969795e8747f75b Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 12 Oct 2021 18:09:02 +0200 Subject: [PATCH 0414/1104] test(ci): more random inputs more random-looking inputs in subtest_fuse_float_binary_operations_correctness and subtest_fuse_float_unary_operations_correctness closes #547 --- .../common/optimization/test_float_fusing.py | 95 ++++++++++++++----- 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 40826ddeb..6118fc178 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -196,15 +196,23 @@ def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): # Some manipulation to avoid issues with domain of definitions of functions if fun == numpy.arccosh: + # 0 is not in the domain of definition input_list = [1, 2, 42, 44] super_fun_list = [mix_x_and_y_and_call_f] elif fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: + # Needs values between 0 and 1 input_list = [0, 0.1, 0.2] super_fun_list = [mix_x_and_y_and_call_f] + elif fun in [numpy.cosh, numpy.sinh, numpy.exp, numpy.exp2, numpy.expm1]: + # Not too large values to avoid overflows + input_list = [1, 2, 5, 11] + super_fun_list = [mix_x_and_y_and_call_f, mix_x_and_y_intricately_and_call_f] elif fun == numpy.invert: + # 0 is not in the domain of definition + expect integer inputs input_list = [1, 2, 42, 44] super_fun_list = [mix_x_and_y_into_integer_and_call_f] else: + # Regular case input_list = [0, 2, 42, 44] super_fun_list = [mix_x_and_y_and_call_f, mix_x_and_y_intricately_and_call_f] @@ -232,20 +240,53 @@ def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): assert fused_num_nodes < orig_num_nodes - ones_input = ( - numpy.ones(tensor_shape, dtype=numpy.dtype(type(input_))) + # Check that the call to the function or to the op_graph evaluation give the same + # result + tensor_diversifier = ( + # The following +1 in the range is to avoid to have 0's which is not in the + # domain definition of some of our functions + numpy.arange(1, numpy.product(tensor_shape) + 1, dtype=numpy.int32).reshape( + tensor_shape + ) if tensor_shape != () else 1 ) - input_ = numpy.int32(input_ * ones_input) + + if fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: + # Domain of definition for these functions + tensor_diversifier = ( + numpy.ones(tensor_shape, dtype=numpy.int32) if tensor_shape != () else 1 + ) + + input_ = numpy.int32(input_ * tensor_diversifier) num_params = len(params_names) - inputs = (input_,) * num_params + assert num_params == 2 - function_result = function_to_trace(*inputs) - op_graph_result = op_graph(*inputs) + # Create inputs which are either of the form [x, x] or [x, y] + for j in range(4): - assert check_results_are_equal(function_result, op_graph_result) + if fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: + if j > 0: + # Domain of definition for these functions + break + + input_a = input_ + input_b = input_ + j + + if tensor_shape != (): + numpy.random.shuffle(input_a) + numpy.random.shuffle(input_b) + + if random.randint(0, 1) == 0: + inputs = (input_a, input_b) + else: + inputs = (input_b, input_a) + + function_result = function_to_trace(*inputs) + op_graph_result = op_graph(*inputs) + + assert check_results_are_equal(function_result, op_graph_result) LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES = { @@ -287,7 +328,7 @@ def subtest_fuse_float_binary_operations_correctness(fun, tensor_shape): # For bivariate functions: fix one of the inputs if i == 0: # With an integer in first position - ones_0 = numpy.ones(tensor_shape, dtype=numpy.int64) if tensor_shape != () else 1 + ones_0 = numpy.ones(tensor_shape, dtype=numpy.int32) if tensor_shape != () else 1 def get_function_to_trace(): return lambda x, y: fun(3 * ones_0, x + y).astype(numpy.float64).astype(numpy.int32) @@ -303,7 +344,7 @@ def subtest_fuse_float_binary_operations_correctness(fun, tensor_shape): elif i == 2: # With an integer in second position - ones_2 = numpy.ones(tensor_shape, dtype=numpy.int64) if tensor_shape != () else 1 + ones_2 = numpy.ones(tensor_shape, dtype=numpy.int32) if tensor_shape != () else 1 def get_function_to_trace(): return lambda x, y: fun(x + y, 4 * ones_2).astype(numpy.float64).astype(numpy.int32) @@ -326,13 +367,6 @@ def subtest_fuse_float_binary_operations_correctness(fun, tensor_shape): input_list = [2, 42, 44] for input_ in input_list: - ones_input = ( - numpy.ones(tensor_shape, dtype=numpy.dtype(type(input_))) - if tensor_shape != () - else 1 - ) - input_ = input_ * ones_input - function_to_trace = get_function_to_trace() params_names = signature(function_to_trace).parameters.keys() @@ -350,15 +384,30 @@ def subtest_fuse_float_binary_operations_correctness(fun, tensor_shape): assert fused_num_nodes < orig_num_nodes - input_ = numpy.int32(input_) + # Check that the call to the function or to the op_graph evaluation give the same + # result + tensor_diversifier = ( + # The following +1 in the range is to avoid to have 0's which is not in the + # domain definition of some of our functions + numpy.arange(1, numpy.product(tensor_shape) + 1, dtype=numpy.int32).reshape( + tensor_shape + ) + if tensor_shape != () + else 1 + ) + input_ = input_ * tensor_diversifier num_params = len(params_names) - inputs = (input_,) * num_params + assert num_params == 2 - function_result = function_to_trace(*inputs) - op_graph_result = op_graph(*inputs) + # Create inputs which are either of the form [x, x] or [x, y] + for j in range(4): + inputs = (input_, input_ + j) - assert check_results_are_equal(function_result, op_graph_result) + function_result = function_to_trace(*inputs) + op_graph_result = op_graph(*inputs) + + assert check_results_are_equal(function_result, op_graph_result) def subtest_fuse_float_binary_operations_dont_support_two_variables(fun, tensor_shape): @@ -383,7 +432,9 @@ def subtest_fuse_float_binary_operations_dont_support_two_variables(fun, tensor_ @pytest.mark.parametrize("fun", tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC) -@pytest.mark.parametrize("tensor_shape", [(), (3, 1, 2)]) +@pytest.mark.parametrize( + "tensor_shape", [pytest.param((), id="scalar"), pytest.param((3, 1, 2), id="tensor")] +) def test_ufunc_operations(fun, tensor_shape): """Test functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" From 67a9bf12ca0e0c91b25e26df4dcd8758e6bb0180 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Tue, 12 Oct 2021 20:07:36 +0200 Subject: [PATCH 0415/1104] test: make that the tested function has integer inputs functions that we compile are supposed to have integer inputs and outputs --- .../common/optimization/test_float_fusing.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 6118fc178..5192081e3 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -75,6 +75,23 @@ def mix_x_and_y_and_call_f(function, x, y): ) +def mix_x_and_y_into_range_0_to_1_and_call_f(function, x, y): + """Mix x and y and then call function, in such a way that the input to function is between + 0 and 1""" + x_p_1 = x + 0.1 + x_p_2 = x + 0.2 + x_p_4 = 1 - numpy.abs(numpy.sin(x_p_1 + x_p_2 + 0.3)) + x_p_3 = function(x_p_4) + return ( + x_p_3.astype(numpy.int32), + x_p_2.astype(numpy.int32), + (x_p_2 + 3).astype(numpy.int32), + x_p_3.astype(numpy.int32) + 67, + y, + (y + 4.7).astype(numpy.int32) + 3, + ) + + def mix_x_and_y_into_integer_and_call_f(function, x, y): """Mix x and y but keep the entry to function as an integer""" x_p_1 = x + 1 @@ -200,9 +217,9 @@ def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): input_list = [1, 2, 42, 44] super_fun_list = [mix_x_and_y_and_call_f] elif fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: - # Needs values between 0 and 1 - input_list = [0, 0.1, 0.2] - super_fun_list = [mix_x_and_y_and_call_f] + # Needs values between 0 and 1 in the call function + input_list = [0, 2, 42, 44] + super_fun_list = [mix_x_and_y_into_range_0_to_1_and_call_f] elif fun in [numpy.cosh, numpy.sinh, numpy.exp, numpy.exp2, numpy.expm1]: # Not too large values to avoid overflows input_list = [1, 2, 5, 11] From 95c48a419c81f419a569835471b16df6399c55c6 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 13 Oct 2021 14:50:37 +0200 Subject: [PATCH 0416/1104] refactor(tracing): preparatory work for tensor table generation - removed underlying_type_constructor from BaseDataType as it was scalar specific and put it in values - update inputset_eval to keep a sample of intermediate node values - allows to get the proper value constructor to be used in UnivariateFunction get_table and have tensors as inputs --- .../bounds_measurement/inputset_eval.py | 20 +++++--- concrete/common/data_types/base.py | 8 ---- concrete/common/data_types/dtypes_helpers.py | 2 +- concrete/common/operator_graph.py | 48 ++++++++++--------- .../common/representation/intermediate.py | 19 ++++---- concrete/common/values/base.py | 3 ++ concrete/numpy/compile.py | 12 ++--- concrete/numpy/np_dtypes_helpers.py | 19 ++++---- .../bounds_measurement/test_inputset_eval.py | 6 +-- tests/conftest.py | 8 ++++ tests/numpy/test_compile.py | 2 +- tests/numpy/test_np_dtypes_helpers.py | 33 +++++++++---- 12 files changed, 102 insertions(+), 78 deletions(-) diff --git a/concrete/common/bounds_measurement/inputset_eval.py b/concrete/common/bounds_measurement/inputset_eval.py index 904c0f009..10d8d91e6 100644 --- a/concrete/common/bounds_measurement/inputset_eval.py +++ b/concrete/common/bounds_measurement/inputset_eval.py @@ -135,7 +135,7 @@ def eval_op_graph_bounds_on_inputset( Returns: Tuple[int, Dict[IntermediateNode, Dict[str, Any]]]: number of inputs in the inputset and a dict containing the bounds for each node from op_graph, stored with the node - as key and a dict with keys "min" and "max" as value. + as key and a dict with keys "min", "max" and "sample" as value. """ def check_inputset_input_len_is_valid(data_to_check): @@ -178,8 +178,12 @@ def eval_op_graph_bounds_on_inputset( # We evaluate the min and max func to be able to resolve the tensors min and max rather than # having the tensor itself as the stored min and max values. - node_bounds = { - node: {"min": min_func(value, value), "max": max_func(value, value)} + node_bounds_and_samples = { + node: { + "min": min_func(value, value), + "max": max_func(value, value), + "sample": value, + } for node, value in first_output.items() } @@ -200,7 +204,11 @@ def eval_op_graph_bounds_on_inputset( current_output = op_graph.evaluate(current_input_data) for node, value in current_output.items(): - node_bounds[node]["min"] = min_func(node_bounds[node]["min"], value) - node_bounds[node]["max"] = max_func(node_bounds[node]["max"], value) + node_bounds_and_samples[node]["min"] = min_func( + node_bounds_and_samples[node]["min"], value + ) + node_bounds_and_samples[node]["max"] = max_func( + node_bounds_and_samples[node]["max"], value + ) - return inputset_size, node_bounds + return inputset_size, node_bounds_and_samples diff --git a/concrete/common/data_types/base.py b/concrete/common/data_types/base.py index dec328fb3..834e75dc9 100644 --- a/concrete/common/data_types/base.py +++ b/concrete/common/data_types/base.py @@ -1,19 +1,11 @@ """File holding code to represent data types in a program.""" from abc import ABC, abstractmethod -from typing import Optional, Type class BaseDataType(ABC): """Base class to represent a data type.""" - # Constructor for the data type represented (for example numpy.int32 for an int32 numpy array) - underlying_type_constructor: Optional[Type] - - def __init__(self) -> None: - super().__init__() - self.underlying_type_constructor = None - @abstractmethod def __eq__(self, o: object) -> bool: """No default implementation.""" diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 86eb7c24f..b1ae53264 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -312,7 +312,7 @@ def get_base_value_for_python_constant_data( return partial(TensorValue, dtype=constant_data_type, shape=()) -def get_type_constructor_for_python_constant_data(constant_data: Union[int, float]): +def get_constructor_for_python_constant_data(constant_data: Union[int, float]): """Get the constructor for the passed python constant data. Args: diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index 92f338984..d7355476b 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -1,14 +1,14 @@ """Code to wrap and make manipulating networkx graphs easier.""" from copy import deepcopy -from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Type, Union +from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Union import networkx as nx from .data_types.base import BaseDataType from .data_types.dtypes_helpers import ( get_base_data_type_for_python_constant_data, - get_type_constructor_for_python_constant_data, + get_constructor_for_python_constant_data, ) from .data_types.floats import Float from .data_types.integers import Integer, make_integer_to_hold @@ -171,15 +171,15 @@ class OPGraph: return node_results - def update_values_with_bounds( + def update_values_with_bounds_and_samples( self, - node_bounds: dict, + node_bounds_and_samples: dict, get_base_data_type_for_constant_data: Callable[ [Any], BaseDataType ] = get_base_data_type_for_python_constant_data, - get_type_constructor_for_constant_data: Callable[ - ..., Type - ] = get_type_constructor_for_python_constant_data, + get_constructor_for_constant_data: Callable[ + ..., Callable + ] = get_constructor_for_python_constant_data, ): """Update values with bounds. @@ -187,40 +187,44 @@ class OPGraph: and passed in nodes_bounds Args: - node_bounds (dict): Dictionary with nodes as keys, holding dicts with a 'min' and 'max' - keys. Those bounds will be taken as the data range to be represented, per node. + node_bounds_and_samples (dict): Dictionary with nodes as keys, holding dicts with a + 'min', 'max' and 'sample' keys. Those bounds will be taken as the data range to be + represented, per node. The sample allows to determine the data constructors to + prepare the UnivariateFunction nodes for table generation. get_base_data_type_for_constant_data (Callable[ [Any], BaseDataType ], optional): This is a callback function to convert data encountered during value updates to BaseDataType. This allows to manage data coming from foreign frameworks without specialising OPGraph. Defaults to get_base_data_type_for_python_constant_data. - get_type_constructor_for_constant_data (Callable[ ..., Type ], optional): This is a + get_constructor_for_constant_data (Callable[ ..., Callable ], optional): This is a callback function to determine the type constructor of the data encountered while - updating the graph bounds. Defaults to get_type_constructor_python_constant_data. + updating the graph bounds. Defaults to get_constructor_for_python_constant_data. """ node: IntermediateNode for node in self.graph.nodes(): - current_node_bounds = node_bounds[node] - min_bound, max_bound = ( - current_node_bounds["min"], - current_node_bounds["max"], + current_node_bounds_and_samples = node_bounds_and_samples[node] + min_bound, max_bound, sample = ( + current_node_bounds_and_samples["min"], + current_node_bounds_and_samples["max"], + current_node_bounds_and_samples["sample"], ) min_data_type = get_base_data_type_for_constant_data(min_bound) max_data_type = get_base_data_type_for_constant_data(max_bound) - min_data_type_constructor = get_type_constructor_for_constant_data(min_bound) - max_data_type_constructor = get_type_constructor_for_constant_data(max_bound) + # This is a sanity check + min_value_constructor = get_constructor_for_constant_data(min_bound) + max_value_constructor = get_constructor_for_constant_data(max_bound) assert_true( - max_data_type_constructor == min_data_type_constructor, + max_value_constructor == min_value_constructor, ( f"Got two different type constructors for min and max bound: " - f"{min_data_type_constructor}, {max_data_type_constructor}" + f"{min_value_constructor}, {max_value_constructor}" ), ) - data_type_constructor = max_data_type_constructor + value_constructor = get_constructor_for_constant_data(sample) if not isinstance(node, Input): for output_value in node.outputs: @@ -238,7 +242,7 @@ class OPGraph: ), ) output_value.dtype = Float(64) - output_value.dtype.underlying_type_constructor = data_type_constructor + output_value.underlying_constructor = value_constructor else: # Currently variable inputs are only allowed to be integers assert_true( @@ -252,7 +256,7 @@ class OPGraph: node.inputs[0].dtype = make_integer_to_hold( (min_bound, max_bound), force_signed=False ) - node.inputs[0].dtype.underlying_type_constructor = data_type_constructor + node.inputs[0].underlying_constructor = value_constructor node.outputs[0] = deepcopy(node.inputs[0]) diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index 144076ffc..f694d61de 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -250,26 +250,25 @@ class UnivariateFunction(IntermediateNode): Returns: List[Any]: The table. """ + input_dtype = self.inputs[0].dtype # Check the input is an unsigned integer to be able to build a table assert isinstance( - self.inputs[0].dtype, Integer + input_dtype, Integer ), "get_table only works for an unsigned Integer input" - assert not self.inputs[ - 0 - ].dtype.is_signed, "get_table only works for an unsigned Integer input" + assert not input_dtype.is_signed, "get_table only works for an unsigned Integer input" - type_constructor = self.inputs[0].dtype.underlying_type_constructor - if type_constructor is None: + input_value_constructor = self.inputs[0].underlying_constructor + if input_value_constructor is None: logger.info( f"{self.__class__.__name__} input data type constructor was None, defaulting to int" ) - type_constructor = int + input_value_constructor = int - min_input_range = self.inputs[0].dtype.min_value() - max_input_range = self.inputs[0].dtype.max_value() + 1 + min_input_range = input_dtype.min_value() + max_input_range = input_dtype.max_value() + 1 table = [ - self.evaluate({0: type_constructor(input_value)}) + self.evaluate({0: input_value_constructor(input_value)}) for input_value in range(min_input_range, max_input_range) ] diff --git a/concrete/common/values/base.py b/concrete/common/values/base.py index 25311f66a..b33f026e4 100644 --- a/concrete/common/values/base.py +++ b/concrete/common/values/base.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from copy import deepcopy +from typing import Callable, Optional from ..data_types.base import BaseDataType @@ -11,10 +12,12 @@ class BaseValue(ABC): dtype: BaseDataType _is_encrypted: bool + underlying_constructor: Optional[Callable] def __init__(self, dtype: BaseDataType, is_encrypted: bool) -> None: self.dtype = deepcopy(dtype) self._is_encrypted = is_encrypted + self.underlying_constructor = None def __repr__(self) -> str: # pragma: no cover return str(self) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 689b9c5b6..f45b4df33 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -26,7 +26,7 @@ from ..numpy.tracing import trace_numpy_function from .np_dtypes_helpers import ( get_base_data_type_for_numpy_or_python_constant_data, get_base_value_for_numpy_or_python_constant_data, - get_type_constructor_for_numpy_or_python_constant_data, + get_constructor_for_numpy_or_python_constant_data, ) @@ -111,7 +111,7 @@ def _compile_numpy_function_into_op_graph_internal( ) # Find bounds with the inputset - inputset_size, node_bounds = eval_op_graph_bounds_on_inputset( + inputset_size, node_bounds_and_samples = eval_op_graph_bounds_on_inputset( op_graph, inputset, compilation_configuration=compilation_configuration, @@ -149,13 +149,13 @@ def _compile_numpy_function_into_op_graph_internal( sys.stderr.write(f"Warning: {message}") # Add the bounds as an artifact - compilation_artifacts.add_final_operation_graph_bounds(node_bounds) + compilation_artifacts.add_final_operation_graph_bounds(node_bounds_and_samples) # Update the graph accordingly: after that, we have the compilable graph - op_graph.update_values_with_bounds( - node_bounds, + op_graph.update_values_with_bounds_and_samples( + node_bounds_and_samples, get_base_data_type_for_numpy_or_python_constant_data, - get_type_constructor_for_numpy_or_python_constant_data, + get_constructor_for_numpy_or_python_constant_data, ) # Add the initial graph as an artifact diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 599886068..24708bb47 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial -from typing import Any, Callable, Dict, List, Type, Union +from typing import Any, Callable, Dict, List, Union import numpy from numpy.typing import DTypeLike @@ -13,7 +13,7 @@ from ..common.data_types.dtypes_helpers import ( find_type_to_hold_both_lossy, get_base_data_type_for_python_constant_data, get_base_value_for_python_constant_data, - get_type_constructor_for_python_constant_data, + get_constructor_for_python_constant_data, ) from ..common.data_types.floats import Float from ..common.data_types.integers import Integer @@ -224,8 +224,8 @@ def get_numpy_function_output_dtype( return [output.dtype for output in outputs] -def get_type_constructor_for_numpy_or_python_constant_data(constant_data: Any): - """Get the constructor for the numpy scalar underlying dtype or python dtype. +def get_constructor_for_numpy_or_python_constant_data(constant_data: Any): + """Get the constructor for the numpy constant data or python dtype. Args: constant_data (Any): The data for which we want to determine the type constructor. @@ -236,11 +236,8 @@ def get_type_constructor_for_numpy_or_python_constant_data(constant_data: Any): f"Unsupported constant data of type {type(constant_data)}", ) - scalar_constructor: Type - if isinstance(constant_data, (numpy.ndarray, SUPPORTED_NUMPY_DTYPES_CLASS_TYPES)): - scalar_constructor = constant_data.dtype.type - else: - scalar_constructor = get_type_constructor_for_python_constant_data(constant_data) - - return scalar_constructor + if isinstance(constant_data, numpy.ndarray): + return lambda x: numpy.full(constant_data.shape, x, dtype=constant_data.dtype) + return constant_data.dtype.type + return get_constructor_for_python_constant_data(constant_data) diff --git a/tests/common/bounds_measurement/test_inputset_eval.py b/tests/common/bounds_measurement/test_inputset_eval.py index 209471873..085ea865c 100644 --- a/tests/common/bounds_measurement/test_inputset_eval.py +++ b/tests/common/bounds_measurement/test_inputset_eval.py @@ -283,17 +283,17 @@ def test_eval_op_graph_bounds_on_inputset_multiple_output( for y_gen in range_y: yield (x_gen, y_gen) - _, node_bounds = eval_op_graph_bounds_on_inputset( + _, node_bounds_and_samples = eval_op_graph_bounds_on_inputset( op_graph, data_gen(*tuple(range(x[0], x[1] + 1) for x in input_ranges)), CompilationConfiguration(), ) for i, output_node in op_graph.output_nodes.items(): - output_node_bounds = node_bounds[output_node] + output_node_bounds = node_bounds_and_samples[output_node] assert (output_node_bounds["min"], output_node_bounds["max"]) == expected_output_bounds[i] - op_graph.update_values_with_bounds(node_bounds) + op_graph.update_values_with_bounds_and_samples(node_bounds_and_samples) for i, output_node in op_graph.output_nodes.items(): assert expected_output_data_type[i] == output_node.outputs[0].dtype diff --git a/tests/conftest.py b/tests/conftest.py index d3e38870b..3a4dc94a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -215,6 +215,14 @@ class TestHelpers: return graphs_are_isomorphic + @staticmethod + def python_functions_are_equal_or_equivalent(lhs, rhs): + """Helper function to check if two functions are equal or their code are equivalent. + + This is not perfect, but will be good enough for tests. + """ + return python_functions_are_equal_or_equivalent(lhs, rhs) + @pytest.fixture def test_helpers(): diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 97563618e..919505e3d 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -330,7 +330,7 @@ def test_fail_compile(function, input_ranges, list_of_arg_names): } with pytest.raises(RuntimeError, match=".*isn't supported for MLIR lowering.*"): - compile_numpy_function_into_op_graph( + compile_numpy_function( function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), diff --git a/tests/numpy/test_np_dtypes_helpers.py b/tests/numpy/test_np_dtypes_helpers.py index a10e2b594..13f8c9ef6 100644 --- a/tests/numpy/test_np_dtypes_helpers.py +++ b/tests/numpy/test_np_dtypes_helpers.py @@ -9,7 +9,7 @@ from concrete.numpy.np_dtypes_helpers import ( convert_base_data_type_to_numpy_dtype, convert_numpy_dtype_to_base_data_type, get_base_value_for_numpy_or_python_constant_data, - get_type_constructor_for_numpy_or_python_constant_data, + get_constructor_for_numpy_or_python_constant_data, ) @@ -65,18 +65,31 @@ def test_convert_common_dtype_to_numpy_dtype(common_dtype, expected_numpy_dtype) (10, int), (42.0, float), (numpy.int32(10), numpy.int32), - (numpy.array([[0, 1], [3, 4]], dtype=numpy.uint64), numpy.uint64), - (numpy.array([[0, 1], [3, 4]], dtype=numpy.float64), numpy.float64), ], ) -def test_get_type_constructor_for_numpy_or_python_constant_data( - constant_data, expected_constructor -): - """Test function for get_type_constructor_for_numpy_or_python_constant_data""" +def test_get_constructor_for_numpy_or_python_constant_data(constant_data, expected_constructor): + """Test function for get_constructor_for_numpy_or_python_constant_data""" - assert expected_constructor == get_type_constructor_for_numpy_or_python_constant_data( - constant_data - ) + assert expected_constructor == get_constructor_for_numpy_or_python_constant_data(constant_data) + + +def test_get_constructor_for_numpy_arrays(test_helpers): + """Test function for get_constructor_for_numpy_or_python_constant_data for numpy arrays.""" + + arrays = [ + numpy.array([[0, 1], [3, 4]], dtype=numpy.uint64), + numpy.array([[0, 1], [3, 4]], dtype=numpy.float64), + ] + + def get_expected_constructor(array: numpy.ndarray): + return lambda x: numpy.full(array.shape, x, dtype=array.dtype) + + expected_constructors = [get_expected_constructor(array) for array in arrays] + + for array, expected_constructor in zip(arrays, expected_constructors): + assert test_helpers.python_functions_are_equal_or_equivalent( + expected_constructor, get_constructor_for_numpy_or_python_constant_data(array) + ) def test_get_base_value_for_numpy_or_python_constant_data_with_list(): From 67f50fb8ce9d1cab2a7d38ae233e7daf117e9242 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 14 Oct 2021 17:04:34 +0300 Subject: [PATCH 0417/1104] refactor(benchmarks): use 'bench' header prefix for all benchmark directives --- benchmarks/124_minus_x.py | 14 ++++----- benchmarks/124_minus_x_tensor.py | 14 ++++----- benchmarks/linear_regression.py | 18 ++++++------ benchmarks/logistic_regression.py | 18 ++++++------ benchmarks/single_table_lookup.py | 14 ++++----- benchmarks/x_minus_1_2_3.py | 14 ++++----- benchmarks/x_minus_1_2_3_broadcasted.py | 14 ++++----- benchmarks/x_minus_24.py | 14 ++++----- benchmarks/x_minus_24_tensor.py | 14 ++++----- benchmarks/x_minus_y.py | 14 ++++----- benchmarks/x_minus_y_broadcasted_tensors.py | 14 ++++----- benchmarks/x_minus_y_tensor_and_scalar.py | 14 ++++----- benchmarks/x_minus_y_tensors.py | 14 ++++----- benchmarks/x_plus_1_2_3.py | 14 ++++----- benchmarks/x_plus_1_2_3_broadcasted.py | 14 ++++----- benchmarks/x_plus_42.py | 14 ++++----- benchmarks/x_plus_42_tensor.py | 14 ++++----- benchmarks/x_plus_y.py | 14 ++++----- benchmarks/x_plus_y_broadcasted_tensors.py | 14 ++++----- benchmarks/x_plus_y_tensor_and_scalar.py | 14 ++++----- benchmarks/x_plus_y_tensors.py | 14 ++++----- benchmarks/x_times_1_2_3.py | 14 ++++----- benchmarks/x_times_1_2_3_broadcasted.py | 14 ++++----- benchmarks/x_times_7.py | 14 ++++----- benchmarks/x_times_7_tensor.py | 14 ++++----- benchmarks/x_times_y.py | 14 ++++----- benchmarks/x_times_y_broadcasted_tensors.py | 14 ++++----- benchmarks/x_times_y_tensor_and_scalar.py | 14 ++++----- benchmarks/x_times_y_tensors.py | 14 ++++----- benchmarks/x_to_the_power_of_2.py | 14 ++++----- script/progress_tracker_utils/measure.py | 32 +++++++++++---------- 31 files changed, 231 insertions(+), 229 deletions(-) diff --git a/benchmarks/124_minus_x.py b/benchmarks/124_minus_x.py index aa74101d0..e178dfdd7 100644 --- a/benchmarks/124_minus_x.py +++ b/benchmarks/124_minus_x.py @@ -1,4 +1,4 @@ -# Unit Target: 124 - x +# bench: Unit Target: 124 - x import random @@ -13,14 +13,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, [(i,) for i in range(2 ** 3)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -32,15 +32,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/124_minus_x_tensor.py b/benchmarks/124_minus_x_tensor.py index 8b2db4834..3fa2c349b 100644 --- a/benchmarks/124_minus_x_tensor.py +++ b/benchmarks/124_minus_x_tensor.py @@ -1,4 +1,4 @@ -# Unit Target: 124 - x (Tensor) +# bench: Unit Target: 124 - x (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 6, size=(3,)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/linear_regression.py b/benchmarks/linear_regression.py index e0d15fb20..5b2f29a0c 100644 --- a/benchmarks/linear_regression.py +++ b/benchmarks/linear_regression.py @@ -1,4 +1,4 @@ -# Full Target: Linear Regression +# bench: Full Target: Linear Regression # Disable line length warnings as we have a looooong metric... # flake8: noqa: E501 @@ -172,14 +172,14 @@ def main(): for x_i in x_q: inputset.append((int(x_i[0]),)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x_0": hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits))}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End non_homomorphic_loss = 0 homomorphic_loss = 0 @@ -198,9 +198,9 @@ def main(): ) .dequantize()[0] ) - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) homomorphic_prediction = QuantizedArray(engine.run(*x_i), y_parameters).dequantize() - # Measure: End + # bench: Measure: End non_homomorphic_loss += (non_homomorphic_prediction - y_i) ** 2 homomorphic_loss += (homomorphic_prediction - y_i) ** 2 @@ -222,10 +222,10 @@ def main(): print(f"Homomorphic Loss: {homomorphic_loss:.4f}") print(f"Relative Difference Percentage: {difference:.2f}%") - # Measure: Non Homomorphic Loss = non_homomorphic_loss - # Measure: Homomorphic Loss = homomorphic_loss - # Measure: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference - # Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 2 + # bench: Measure: Non Homomorphic Loss = non_homomorphic_loss + # bench: Measure: Homomorphic Loss = homomorphic_loss + # bench: Measure: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference + # bench: Alert: Relative Loss Difference Between Homomorphic and Non Homomorphic Implementation (%) > 2 if __name__ == "__main__": diff --git a/benchmarks/logistic_regression.py b/benchmarks/logistic_regression.py index bd9f95a42..805f8f783 100644 --- a/benchmarks/logistic_regression.py +++ b/benchmarks/logistic_regression.py @@ -1,4 +1,4 @@ -# Full Target: Logistic Regression +# bench: Full Target: Logistic Regression # Disable line length warnings as we have a looooong metric... # flake8: noqa: E501 @@ -244,7 +244,7 @@ def main(): for x_i in x_q: inputset.append((int(x_i[0]), int(x_i[1]))) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, { @@ -254,7 +254,7 @@ def main(): inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End non_homomorphic_correct = 0 homomorphic_correct = 0 @@ -273,9 +273,9 @@ def main(): ) ).dequantize()[0] ) - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) homomorphic_prediction = round(QuantizedArray(engine.run(*x_i), y_parameters).dequantize()) - # Measure: End + # bench: Measure: End if non_homomorphic_prediction == y_i: non_homomorphic_correct += 1 @@ -299,10 +299,10 @@ def main(): print(f"Homomorphic Accuracy: {homomorphic_accuracy:.4f}") print(f"Difference Percentage: {difference:.2f}%") - # Measure: Non Homomorphic Accuracy = non_homomorphic_accuracy - # Measure: Homomorphic Accuracy = homomorphic_accuracy - # Measure: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference - # Alert: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) > 2 + # bench: Measure: Non Homomorphic Accuracy = non_homomorphic_accuracy + # bench: Measure: Homomorphic Accuracy = homomorphic_accuracy + # bench: Measure: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) = difference + # bench: Alert: Accuracy Difference Between Homomorphic and Non Homomorphic Implementation (%) > 2 if __name__ == "__main__": diff --git a/benchmarks/single_table_lookup.py b/benchmarks/single_table_lookup.py index 2b650319a..5db7af140 100644 --- a/benchmarks/single_table_lookup.py +++ b/benchmarks/single_table_lookup.py @@ -1,4 +1,4 @@ -# Unit Target: Single Table Lookup +# bench: Unit Target: Single Table Lookup import random @@ -18,14 +18,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(input_bits)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, [(i,) for i in range(2 ** input_bits)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_1_2_3.py b/benchmarks/x_minus_1_2_3.py index 9b1f59071..68e1db305 100644 --- a/benchmarks/x_minus_1_2_3.py +++ b/benchmarks/x_minus_1_2_3.py @@ -1,4 +1,4 @@ -# Unit Target: x - [1, 2, 3] +# bench: Unit Target: x - [1, 2, 3] import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 2, size=(3,)) + np.array([1, 2, 3]),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_1_2_3_broadcasted.py b/benchmarks/x_minus_1_2_3_broadcasted.py index 64f583384..7641653ab 100644 --- a/benchmarks/x_minus_1_2_3_broadcasted.py +++ b/benchmarks/x_minus_1_2_3_broadcasted.py @@ -1,4 +1,4 @@ -# Unit Target: x - [1, 2, 3] (Broadcasted) +# bench: Unit Target: x - [1, 2, 3] (Broadcasted) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -16,14 +16,14 @@ def main(): (np.random.randint(0, 2 ** 2, size=(2, 3)) + np.array([1, 2, 3]),) for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -35,15 +35,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_24.py b/benchmarks/x_minus_24.py index 2b03e8d8d..8efceaec0 100644 --- a/benchmarks/x_minus_24.py +++ b/benchmarks/x_minus_24.py @@ -1,4 +1,4 @@ -# Unit Target: x - 24 +# bench: Unit Target: x - 24 import random @@ -13,14 +13,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(6)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, [(i,) for i in range(24, 2 ** 6)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -32,15 +32,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_24_tensor.py b/benchmarks/x_minus_24_tensor.py index 06bd641b7..209717586 100644 --- a/benchmarks/x_minus_24_tensor.py +++ b/benchmarks/x_minus_24_tensor.py @@ -1,4 +1,4 @@ -# Unit Target: x - 24 (Tensor) +# bench: Unit Target: x - 24 (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 5, size=(3,)) + 24,) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y.py b/benchmarks/x_minus_y.py index 3f6413bda..cc6fee277 100644 --- a/benchmarks/x_minus_y.py +++ b/benchmarks/x_minus_y.py @@ -1,4 +1,4 @@ -# Unit Target: x - y +# bench: Unit Target: x - y import itertools import random @@ -17,14 +17,14 @@ def main(): inputset = itertools.product(range(4, 8), range(0, 4)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y_broadcasted_tensors.py b/benchmarks/x_minus_y_broadcasted_tensors.py index c9b7b9077..bb88176f4 100644 --- a/benchmarks/x_minus_y_broadcasted_tensors.py +++ b/benchmarks/x_minus_y_broadcasted_tensors.py @@ -1,4 +1,4 @@ -# Unit Target: x - y (Broadcasted Tensors) +# bench: Unit Target: x - y (Broadcasted Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -18,14 +18,14 @@ def main(): for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -38,15 +38,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y_tensor_and_scalar.py b/benchmarks/x_minus_y_tensor_and_scalar.py index 56f118d2d..480ee10ea 100644 --- a/benchmarks/x_minus_y_tensor_and_scalar.py +++ b/benchmarks/x_minus_y_tensor_and_scalar.py @@ -1,4 +1,4 @@ -# Unit Target: x - y (Tensor & Scalar) +# bench: Unit Target: x - y (Tensor & Scalar) import random @@ -17,14 +17,14 @@ def main(): inputset = [(np.random.randint(4, 8, size=(3,)), random.randint(0, 3)) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_minus_y_tensors.py b/benchmarks/x_minus_y_tensors.py index f6f1ad44c..ab8892d21 100644 --- a/benchmarks/x_minus_y_tensors.py +++ b/benchmarks/x_minus_y_tensors.py @@ -1,4 +1,4 @@ -# Unit Target: x - y (Tensors) +# bench: Unit Target: x - y (Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -17,14 +17,14 @@ def main(): (np.random.randint(4, 8, size=(3,)), np.random.randint(0, 4, size=(3,))) for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_1_2_3.py b/benchmarks/x_plus_1_2_3.py index 2292e3460..46a6f7742 100644 --- a/benchmarks/x_plus_1_2_3.py +++ b/benchmarks/x_plus_1_2_3.py @@ -1,4 +1,4 @@ -# Unit Target: x + [1, 2, 3] +# bench: Unit Target: x + [1, 2, 3] import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_1_2_3_broadcasted.py b/benchmarks/x_plus_1_2_3_broadcasted.py index eb5e156ac..b1865cec6 100644 --- a/benchmarks/x_plus_1_2_3_broadcasted.py +++ b/benchmarks/x_plus_1_2_3_broadcasted.py @@ -1,4 +1,4 @@ -# Unit Target: x + [1, 2, 3] (Broadcasted) +# bench: Unit Target: x + [1, 2, 3] (Broadcasted) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 3, size=(2, 3)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index 41b27b36a..9b28ddff3 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -1,4 +1,4 @@ -# Unit Target: x + 42 +# bench: Unit Target: x + 42 import random @@ -13,14 +13,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, [(i,) for i in range(2 ** 3)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -32,15 +32,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_42_tensor.py b/benchmarks/x_plus_42_tensor.py index 9395926a1..2bbf7acc9 100644 --- a/benchmarks/x_plus_42_tensor.py +++ b/benchmarks/x_plus_42_tensor.py @@ -1,4 +1,4 @@ -# Unit Target: x + 42 (Tensor) +# bench: Unit Target: x + 42 (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y.py b/benchmarks/x_plus_y.py index bdf944d92..e58f5f0d9 100644 --- a/benchmarks/x_plus_y.py +++ b/benchmarks/x_plus_y.py @@ -1,4 +1,4 @@ -# Unit Target: x + y +# bench: Unit Target: x + y import random @@ -14,14 +14,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) y = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, [(random.randint(0, 7), random.randint(0, 7)) for _ in range(32)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -34,15 +34,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y_broadcasted_tensors.py b/benchmarks/x_plus_y_broadcasted_tensors.py index e3960e895..a917e3c52 100644 --- a/benchmarks/x_plus_y_broadcasted_tensors.py +++ b/benchmarks/x_plus_y_broadcasted_tensors.py @@ -1,4 +1,4 @@ -# Unit Target: x + y (Broadcasted Tensors) +# bench: Unit Target: x + y (Broadcasted Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -18,14 +18,14 @@ def main(): for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -38,15 +38,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y_tensor_and_scalar.py b/benchmarks/x_plus_y_tensor_and_scalar.py index 8de52337c..fd108c0dd 100644 --- a/benchmarks/x_plus_y_tensor_and_scalar.py +++ b/benchmarks/x_plus_y_tensor_and_scalar.py @@ -1,4 +1,4 @@ -# Unit Target: x + y (Tensor & Scalar) +# bench: Unit Target: x + y (Tensor & Scalar) import random @@ -17,14 +17,14 @@ def main(): inputset = [(np.random.randint(0, 8, size=(3,)), random.randint(0, 7)) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_plus_y_tensors.py b/benchmarks/x_plus_y_tensors.py index 79693d82f..6f9c1970d 100644 --- a/benchmarks/x_plus_y_tensors.py +++ b/benchmarks/x_plus_y_tensors.py @@ -1,4 +1,4 @@ -# Unit Target: x + y (Tensors) +# bench: Unit Target: x + y (Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -18,14 +18,14 @@ def main(): for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -38,15 +38,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_1_2_3.py b/benchmarks/x_times_1_2_3.py index c6bd26c7d..839509e62 100644 --- a/benchmarks/x_times_1_2_3.py +++ b/benchmarks/x_times_1_2_3.py @@ -1,4 +1,4 @@ -# Unit Target: x * [1, 2, 3] +# bench: Unit Target: x * [1, 2, 3] import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_1_2_3_broadcasted.py b/benchmarks/x_times_1_2_3_broadcasted.py index 41e2b25f7..fcd5299c0 100644 --- a/benchmarks/x_times_1_2_3_broadcasted.py +++ b/benchmarks/x_times_1_2_3_broadcasted.py @@ -1,4 +1,4 @@ -# Unit Target: x * [1, 2, 3] (Broadcasted) +# bench: Unit Target: x * [1, 2, 3] (Broadcasted) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 3, size=(2, 3)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_7.py b/benchmarks/x_times_7.py index f82454750..2c8ba90c1 100644 --- a/benchmarks/x_times_7.py +++ b/benchmarks/x_times_7.py @@ -1,4 +1,4 @@ -# Unit Target: x * 7 +# bench: Unit Target: x * 7 import random @@ -13,14 +13,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(4)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, [(i,) for i in range(2 ** 4)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -32,15 +32,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_7_tensor.py b/benchmarks/x_times_7_tensor.py index f65cb6e92..ef0bd900b 100644 --- a/benchmarks/x_times_7_tensor.py +++ b/benchmarks/x_times_7_tensor.py @@ -1,4 +1,4 @@ -# Unit Target: x * 7 (Tensor) +# bench: Unit Target: x * 7 (Tensor) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -14,14 +14,14 @@ def main(): inputset = [(np.random.randint(0, 2 ** 3, size=(3,)),) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -33,15 +33,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y.py b/benchmarks/x_times_y.py index fd8092ffc..b7c261cb3 100644 --- a/benchmarks/x_times_y.py +++ b/benchmarks/x_times_y.py @@ -1,4 +1,4 @@ -# Unit Target: x * y +# bench: Unit Target: x * y import itertools import random @@ -17,14 +17,14 @@ def main(): inputset = itertools.product(range(4, 8), range(0, 4)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y_broadcasted_tensors.py b/benchmarks/x_times_y_broadcasted_tensors.py index 18d50a74e..65ec15ce0 100644 --- a/benchmarks/x_times_y_broadcasted_tensors.py +++ b/benchmarks/x_times_y_broadcasted_tensors.py @@ -1,4 +1,4 @@ -# Unit Target: x * y (Broadcasted Tensors) +# bench: Unit Target: x * y (Broadcasted Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -18,14 +18,14 @@ def main(): for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -38,15 +38,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y_tensor_and_scalar.py b/benchmarks/x_times_y_tensor_and_scalar.py index bd2cccaac..878bbae00 100644 --- a/benchmarks/x_times_y_tensor_and_scalar.py +++ b/benchmarks/x_times_y_tensor_and_scalar.py @@ -1,4 +1,4 @@ -# Unit Target: x * y (Tensor & Scalar) +# bench: Unit Target: x * y (Tensor & Scalar) import random @@ -17,14 +17,14 @@ def main(): inputset = [(np.random.randint(0, 8, size=(3,)), random.randint(0, 7)) for _ in range(32)] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -37,15 +37,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_times_y_tensors.py b/benchmarks/x_times_y_tensors.py index adefc6b4b..ca94ee518 100644 --- a/benchmarks/x_times_y_tensors.py +++ b/benchmarks/x_times_y_tensors.py @@ -1,4 +1,4 @@ -# Unit Target: x * y (Tensors) +# bench: Unit Target: x * y (Tensors) import numpy as np from common import BENCHMARK_CONFIGURATION @@ -18,14 +18,14 @@ def main(): for _ in range(32) ] - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x, "y": y}, inputset, compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -38,15 +38,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/benchmarks/x_to_the_power_of_2.py b/benchmarks/x_to_the_power_of_2.py index 61a5f4577..863b3256f 100644 --- a/benchmarks/x_to_the_power_of_2.py +++ b/benchmarks/x_to_the_power_of_2.py @@ -1,4 +1,4 @@ -# Unit Target: x**2 +# bench: Unit Target: x**2 import random @@ -13,14 +13,14 @@ def main(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) - # Measure: Compilation Time (ms) + # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, [(i,) for i in range(2 ** 3)], compilation_configuration=BENCHMARK_CONFIGURATION, ) - # Measure: End + # bench: Measure: End inputs = [] labels = [] @@ -32,15 +32,15 @@ def main(): correct = 0 for input_i, label_i in zip(inputs, labels): - # Measure: Evaluation Time (ms) + # bench: Measure: Evaluation Time (ms) result_i = engine.run(*input_i) - # Measure: End + # bench: Measure: End if result_i == label_i: correct += 1 - # Measure: Accuracy (%) = (correct / len(inputs)) * 100 - # Alert: Accuracy (%) != 100 + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 if __name__ == "__main__": diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 069d688fb..01e91aa47 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -31,7 +31,7 @@ def register_alert(script, index, line, metrics, alerts): """Parse line, check its correctness, add it to list of alerts if it's valid""" # Extract the alert details - alert_line = line.replace("# Alert:", "") + alert_line = line.replace("# bench: Alert:", "") # Parse the alert and append it to list of alerts supported_operators = ["==", "!=", "<=", ">=", "<", ">"] @@ -89,7 +89,7 @@ def identify_metrics_and_alerts(script, lines, metrics, alerts): line = line.strip() # Check whether the line is a special line or not - if line == "# Measure: End": + if line == "# bench: Measure: End": # Make sure a measurement is active already if not in_measurement: raise SyntaxError( @@ -107,7 +107,7 @@ def identify_metrics_and_alerts(script, lines, metrics, alerts): # Set in_measurement to false as the active measurement has ended in_measurement = False - elif line.startswith("# Measure:"): + elif line.startswith("# bench: Measure:"): # Make sure a measurement is not active already if in_measurement: raise SyntaxError( @@ -116,7 +116,7 @@ def identify_metrics_and_alerts(script, lines, metrics, alerts): ) # Extract the measurement details - measurement_details = line.replace("# Measure:", "").split("=") + measurement_details = line.replace("# bench: Measure:", "").split("=") # Extract metric name and id metric_label = measurement_details[0].strip() @@ -131,7 +131,7 @@ def identify_metrics_and_alerts(script, lines, metrics, alerts): in_measurement = True measurement_line = index + 1 measurement_indentation = indentation - elif line.startswith("# Alert:"): + elif line.startswith("# bench: Alert:"): register_alert(script, index, line, metrics, alerts) # Make sure there isn't an active measurement that hasn't finished @@ -164,20 +164,20 @@ def create_modified_script(script, lines, metrics): # Copy the lines of the original script into the new script for line in lines[1:]: # And modify special lines along the way - if line.strip() == "# Measure: End": + if line.strip() == "# bench: Measure: End": # Replace `# Measure: End` with # # _end_ = time.time() # _measurements_["id"].append((_end_ - _start_) * 1000) - index = line.find("# Measure: End") + index = line.find("# bench: Measure: End") line = line[:index] f.write(f"{line}_end_ = time.time()\n") value = "(_end_ - _start_) * 1000" line += f'_measurements_["{current_metric_id}"].append({value})\n' - elif line.strip().startswith("# Measure:"): + elif line.strip().startswith("# bench: Measure:"): # Replace `# Measure: ...` with # # _start_ = time.time() @@ -186,11 +186,11 @@ def create_modified_script(script, lines, metrics): # # _measurements_["id"].append(expression) - metric_details = line.replace("# Measure:", "").split("=") + metric_details = line.replace("# bench: Measure:", "").split("=") metric_label = metric_details[0].strip() metric_id = name_to_id(metric_label) - index = line.find("# Measure:") + index = line.find("# bench: Measure:") line = line[:index] if len(metric_details) == 1: @@ -321,13 +321,13 @@ def main(args): break # Check whether the script is a target or not - if first_line.startswith("# Unit Target:"): + if first_line.startswith("# bench: Unit Target:"): # Extract target name - target_name = first_line.replace("# Unit Target:", "").strip() + target_name = first_line.replace("# bench: Unit Target:", "").strip() is_unit = True - elif first_line.startswith("# Full Target:"): + elif first_line.startswith("# bench: Full Target:"): # Extract target name - target_name = first_line.replace("# Full Target:", "").strip() + target_name = first_line.replace("# bench: Full Target:", "").strip() is_unit = False else: print() @@ -337,7 +337,9 @@ def main(args): with tqdm.tqdm(total=samples) as pbar: pbar.write(" Sample 1") pbar.write(" --------") - pbar.write(" Skipped (doesn't have a `# Target:` directive)\n") + pbar.write( + " Skipped (doesn't have a `# bench: Unit/Full Target:` directive)\n" + ) pbar.update(samples) print() From 12465f86ace467c402ec8ef85e21d88bd1bad6c0 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 13 Oct 2021 14:55:49 +0200 Subject: [PATCH 0418/1104] test: check correctness of ufunc's for the moment: - it has no hard check of correctness for now - some functions are not managed ref #551 --- tests/numpy/test_compile.py | 247 ++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 919505e3d..9af843c9d 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -10,6 +10,7 @@ from concrete.common.data_types.integers import Integer from concrete.common.debugging import draw_graph, get_printable_graph from concrete.common.extensions.table import LookupTable from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor +from concrete.numpy import tracing from concrete.numpy.compile import compile_numpy_function, compile_numpy_function_into_op_graph @@ -52,6 +53,252 @@ def complicated_topology(x): ) +def mix_x_and_y_and_call_f(func, x, y): + """Create an upper function to test `func`""" + z = numpy.abs(10 * func(x)) + z = z.astype(numpy.int32) + y + return z + + +def mix_x_and_y_and_call_f_with_float_inputs(func, x, y): + """Create an upper function to test `func`, with inputs which are forced to be floats""" + z = numpy.abs(10 * func(x + 0.1)) + z = z.astype(numpy.int32) + y + return z + + +def mix_x_and_y_and_call_f_with_integer_inputs(func, x, y): + """Create an upper function to test `func`, with inputs which are forced to be integers but + in a way which is fusable into a TLU""" + a = x + 0.1 + a = numpy.rint(a).astype(numpy.int32) + z = numpy.abs(10 * func(a)) + z = z.astype(numpy.int32) + y + return z + + +def mix_x_and_y_and_call_f_which_expects_small_inputs(func, x, y): + """Create an upper function to test `func`, which expects small values to not use too much + precision""" + a = numpy.abs(0.77 * numpy.sin(x)) + z = numpy.abs(3 * func(a)) + z = z.astype(numpy.int32) + y + return z + + +def mix_x_and_y_and_call_f_which_has_large_outputs(func, x, y): + """Create an upper function to test `func`, which outputs large values""" + a = numpy.abs(2 * numpy.sin(x)) + z = numpy.abs(func(a) * 0.131) + z = z.astype(numpy.int32) + y + return z + + +def mix_x_and_y_and_call_f_avoid_0_input(func, x, y): + """Create an upper function to test `func`, which makes that inputs are not 0""" + a = numpy.abs(7 * numpy.sin(x)) + 1 + z = numpy.abs(5 * func(a)) + z = z.astype(numpy.int32) + y + return z + + +def mix_x_and_y_and_call_binary_f_one(func, c, x, y): + """Create an upper function to test `func`""" + z = numpy.abs(func(x, c) + 1) + z = z.astype(numpy.uint32) + y + return z + + +def mix_x_and_y_and_call_binary_f_two(func, c, x, y): + """Create an upper function to test `func`""" + z = numpy.abs(func(c, x) + 1) + z = z.astype(numpy.uint32) + y + return z + + +def mix_x_and_y_and_call_binary_f_two_avoid_0_input(func, c, x, y): + """Create an upper function to test `func`""" + z = numpy.abs(func(c, x + 1) + 1) + z = z.astype(numpy.uint32) + y + return z + + +def subtest_compile_and_run_unary_ufunc_correctness(ufunc, upper_function, input_ranges): + """Test correctness of results when running a compiled function""" + + def get_function(ufunc, upper_function): + return lambda x, y: upper_function(ufunc, x, y) + + function = get_function(ufunc, upper_function) + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = {arg_name: EncryptedScalar(Integer(64, False)) for arg_name in ["x", "y"]} + + compiler_engine = compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + args = [random.randint(low, high) for (low, high) in input_ranges] + + # TODO: fix the check + # assert compiler_engine.run(*args) == function(*args) + + if compiler_engine.run(*args) != function(*args): + print("Warning, bad computation") + + +def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, input_ranges): + """Test correctness of results when running a compiled function""" + + def get_function(ufunc, upper_function): + return lambda x, y: upper_function(ufunc, c, x, y) + + function = get_function(ufunc, upper_function) + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = {arg_name: EncryptedScalar(Integer(64, False)) for arg_name in ["x", "y"]} + + compiler_engine = compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + args = [random.randint(low, high) for (low, high) in input_ranges] + + # TODO: fix the check + # assert compiler_engine.run(*args) == function(*args) + + if compiler_engine.run(*args) != function(*args): + print("Warning, bad computation") + + +@pytest.mark.parametrize( + "ufunc", + [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 2], +) +def test_binary_ufunc_operations(ufunc): + """Test biary functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" + if ufunc in [numpy.power, numpy.float_power]: + # Need small constants to keep results really small + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_one, 3, ((0, 4), (0, 5)) + ) + elif ufunc in [numpy.lcm, numpy.left_shift]: + # Need small constants to keep results sufficiently small + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_one, 3, ((0, 5), (0, 5)) + ) + else: + # General case + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_one, 41, ((0, 5), (0, 5)) + ) + + if ufunc in [numpy.power, numpy.float_power]: + # Need small constants to keep results really small + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 4), (0, 5)) + ) + elif ufunc in [numpy.floor_divide, numpy.fmod, numpy.remainder, numpy.true_divide]: + # 0 not in the domain of definition + # Can't make it work, #649 + # TODO: fixme + pass + # subtest_compile_and_run_binary_ufunc_correctness( + # ufunc, mix_x_and_y_and_call_binary_f_two_avoid_0_input, 31, ((1, 5), (1, 5)) + # ) + elif ufunc in [numpy.lcm, numpy.left_shift]: + # Need small constants to keep results sufficiently small + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) + ) + elif ufunc in [numpy.ldexp]: + # Can't make it work + # TODO: fixme + pass + + # Need small constants to keep results sufficiently small + # subtest_compile_and_run_binary_ufunc_correctness( + # ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) + # ) + else: + # General case + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_two, 42, ((0, 5), (0, 5)) + ) + + +@pytest.mark.parametrize( + "ufunc", [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1] +) +def test_unary_ufunc_operations(ufunc): + """Test unary functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" + if ufunc in [ + numpy.degrees, + numpy.rad2deg, + ]: + # Need to reduce the output value, to avoid to need too much precision + subtest_compile_and_run_unary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_f_which_has_large_outputs, ((0, 5), (0, 5)) + ) + elif ufunc in [ + numpy.negative, + ]: + # Need to turn the input into a float + subtest_compile_and_run_unary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_f_with_float_inputs, ((0, 5), (0, 5)) + ) + elif ufunc in [ + numpy.invert, + ]: + # Can't make it work, to have a fusable function + # TODO: fixme + pass + # subtest_compile_and_run_unary_ufunc_correctness( + # ufunc, mix_x_and_y_and_call_f_with_integer_inputs, ((0, 5), (0, 5)) + # ) + elif ufunc in [ + numpy.arccosh, + numpy.log, + numpy.log2, + numpy.log10, + numpy.reciprocal, + ]: + # No 0 in the domain of definition + subtest_compile_and_run_unary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_f_avoid_0_input, ((1, 5), (1, 5)) + ) + elif ufunc in [ + numpy.cosh, + numpy.exp, + numpy.exp2, + numpy.expm1, + numpy.square, + numpy.arccos, + numpy.arcsin, + numpy.arctanh, + numpy.sinh, + ]: + # Need a small range of inputs, to avoid to need too much precision + subtest_compile_and_run_unary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_f_which_expects_small_inputs, ((0, 5), (0, 5)) + ) + else: + # Regular case for univariate functions + subtest_compile_and_run_unary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_f, ((0, 5), (0, 5)) + ) + + @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ From 753ab5b6a252a83b3ef517024ee6502635e40626 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 14 Oct 2021 15:59:26 +0300 Subject: [PATCH 0419/1104] feat(ci): add 'compilation' optional scope --- .github/workflows/continuous-integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 6eb086dd9..292fcf0a0 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -195,10 +195,10 @@ jobs: if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: - pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts)\))?\:) .+$' + pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation)\))?\:) .+$' flags: 'gs' error: "Your first line has to contain a commit type and scope like \"feat(my_feature): msg\".\ - Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts)\\))?\\:)'" + Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation)\\))?\\:)'" excludeDescription: 'true' # optional: this excludes the description body of a pull request excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request From 4c9c49ecd23436e870e4ba4f46fa42172e4bfa1d Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 15 Oct 2021 11:36:31 +0300 Subject: [PATCH 0420/1104] feat(debugging): provide a way for highlighting nodes with custom messages during printing --- concrete/common/debugging/printing.py | 22 ++++-- tests/common/debugging/test_printing.py | 91 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 tests/common/debugging/test_printing.py diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 688a4faef..0077ce794 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -1,12 +1,12 @@ """functions to print the different graphs we can generate in the package, eg to debug.""" -from typing import Any, Dict +from typing import Any, Dict, Optional import networkx as nx from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph -from ..representation.intermediate import Constant, Input, UnivariateFunction +from ..representation.intermediate import Constant, Input, IntermediateNode, UnivariateFunction def output_data_type_to_string(node): @@ -39,18 +39,26 @@ def shorten_a_constant(constant_data: str): return short_content -def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: +def get_printable_graph( + opgraph: OPGraph, + show_data_types: bool = False, + highlighted_nodes: Optional[Dict[IntermediateNode, str]] = None, +) -> str: """Return a string representing a graph. Args: opgraph (OPGraph): The graph that we want to draw - show_data_types (bool): Whether or not showing data_types of nodes, eg - to see their width + show_data_types (bool): Whether or not showing data_types of nodes, eg to see their width + highlighted_nodes (Optional[Dict[IntermediateNode, str]]): + The dict of nodes which will be highlighted and their corresponding messages Returns: str: a string to print or save in a file """ assert_true(isinstance(opgraph, OPGraph)) + + highlighted_nodes = highlighted_nodes if highlighted_nodes is not None else {} + list_of_nodes_which_are_outputs = list(opgraph.output_nodes.values()) graph = opgraph.graph @@ -127,6 +135,10 @@ def get_printable_graph(opgraph: OPGraph, show_data_types: bool = False) -> str: returned_str += f"{new_line}\n" + if node in highlighted_nodes: + message = highlighted_nodes[node] + returned_str += f"{'^' * len(new_line)} {message}\n" + map_table[node] = i i += 1 diff --git a/tests/common/debugging/test_printing.py b/tests/common/debugging/test_printing.py new file mode 100644 index 000000000..d183329ca --- /dev/null +++ b/tests/common/debugging/test_printing.py @@ -0,0 +1,91 @@ +"""Test file for printing""" + +from concrete.common.data_types.integers import Integer +from concrete.common.debugging import get_printable_graph +from concrete.common.values import EncryptedScalar +from concrete.numpy.compile import compile_numpy_function_into_op_graph + + +def test_get_printable_graph_with_offending_nodes(): + """Test get_printable_graph with offending nodes""" + + def function(x): + return x + 42 + + opgraph = compile_numpy_function_into_op_graph( + function, + {"x": EncryptedScalar(Integer(7, True))}, + [(i,) for i in range(-5, 5)], + ) + + highlighted_nodes = {opgraph.input_nodes[0]: "foo"} + + without_types = get_printable_graph( + opgraph, show_data_types=False, highlighted_nodes=highlighted_nodes + ).strip() + with_types = get_printable_graph( + opgraph, show_data_types=True, highlighted_nodes=highlighted_nodes + ).strip() + + assert ( + without_types + == """ + +%0 = x +^^^^^^ foo +%1 = Constant(42) +%2 = Add(%0, %1) +return(%2) + +""".strip() + ) + + assert ( + with_types + == """ + +%0 = x # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ foo +%1 = Constant(42) # ClearScalar> +%2 = Add(%0, %1) # EncryptedScalar> +return(%2) + +""".strip() + ) + + highlighted_nodes = {opgraph.input_nodes[0]: "foo", opgraph.output_nodes[0]: "bar"} + + without_types = get_printable_graph( + opgraph, show_data_types=False, highlighted_nodes=highlighted_nodes + ).strip() + with_types = get_printable_graph( + opgraph, show_data_types=True, highlighted_nodes=highlighted_nodes + ).strip() + + assert ( + without_types + == """ + +%0 = x +^^^^^^ foo +%1 = Constant(42) +%2 = Add(%0, %1) +^^^^^^^^^^^^^^^^ bar +return(%2) + +""".strip() + ) + + assert ( + with_types + == """ + +%0 = x # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ foo +%1 = Constant(42) # ClearScalar> +%2 = Add(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ bar +return(%2) + +""".strip() + ) From 73769b917ee35b00c383d3035e2d0e65ddb66f38 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 15 Oct 2021 11:36:54 +0300 Subject: [PATCH 0421/1104] feat(compilation): provide the reason for MLIR incompatibility --- concrete/common/data_types/dtypes_helpers.py | 24 ++++++++++ concrete/common/mlir/utils.py | 33 ++++++++----- concrete/numpy/compile.py | 11 +++-- tests/numpy/test_compile.py | 49 ++++++++++++++------ 4 files changed, 88 insertions(+), 29 deletions(-) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index b1ae53264..bf48470ca 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -71,6 +71,30 @@ def value_is_scalar_integer(value_to_check: BaseValue) -> bool: ) +def value_is_scalar(value_to_check: BaseValue) -> bool: + """Check that a value is a scalar. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is a scalar + """ + return isinstance(value_to_check, TensorValue) and value_to_check.is_scalar + + +def value_is_integer(value_to_check: BaseValue) -> bool: + """Check that a value is of type Integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is of type Integer + """ + return isinstance(value_to_check.dtype, INTEGER_TYPES) + + def value_is_encrypted_tensor_integer(value_to_check: BaseValue) -> bool: """Check that a value is an encrypted TensorValue of type Integer. diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 6c37cddf1..ad2e302b8 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -1,5 +1,5 @@ """Utilities for MLIR conversion.""" -from typing import cast +from typing import Dict, Optional, cast from ..data_types import Integer from ..data_types.dtypes_helpers import ( @@ -7,32 +7,41 @@ from ..data_types.dtypes_helpers import ( value_is_clear_tensor_integer, value_is_encrypted_scalar_integer, value_is_encrypted_tensor_integer, - value_is_scalar_integer, + value_is_integer, + value_is_scalar, ) from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph -from ..representation.intermediate import UnivariateFunction +from ..representation.intermediate import IntermediateNode, UnivariateFunction # TODO: should come from compiler, through an API, #402 ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB = 7 -def is_graph_values_compatible_with_mlir(op_graph: OPGraph) -> bool: +def check_graph_values_compatibility_with_mlir( + op_graph: OPGraph, +) -> Optional[Dict[IntermediateNode, str]]: """Make sure the graph outputs are unsigned integers, which is what the compiler supports. Args: op_graph: computation graph to check Returns: - bool: is the graph compatible with the expected MLIR representation + Dict[IntermediateNode, str]: None if the graph is compatible + information about offending nodes otherwise """ - return all( - all( - value_is_scalar_integer(out) and not cast(Integer, out.dtype).is_signed - for out in out_node.outputs - ) - for out_node in op_graph.output_nodes.values() - ) + + offending_nodes = {} + + for out_node in op_graph.output_nodes.values(): + for out in out_node.outputs: + if not value_is_scalar(out): + offending_nodes[out_node] = "non scalar outputs aren't supported" + + if value_is_integer(out) and cast(Integer, out.dtype).is_signed: + offending_nodes[out_node] = "signed integer outputs aren't supported" + + return None if len(offending_nodes) == 0 else offending_nodes def _set_all_bit_width(op_graph: OPGraph, p: int): diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index f45b4df33..fa64e54c3 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -11,11 +11,12 @@ from ..common.bounds_measurement.inputset_eval import eval_op_graph_bounds_on_in from ..common.common_helpers import check_op_graph_is_integer_program from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.data_types import Integer +from ..common.debugging import get_printable_graph from ..common.fhe_circuit import FHECircuit from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter from ..common.mlir.utils import ( + check_graph_values_compatibility_with_mlir, extend_direct_lookup_tables, - is_graph_values_compatible_with_mlir, update_bit_width_for_mlir, ) from ..common.operator_graph import OPGraph @@ -162,8 +163,12 @@ def _compile_numpy_function_into_op_graph_internal( compilation_artifacts.add_operation_graph("final", op_graph) # Make sure the graph can be lowered to MLIR - if not is_graph_values_compatible_with_mlir(op_graph): - raise RuntimeError("function you are trying to compile isn't supported for MLIR lowering") + offending_nodes = check_graph_values_compatibility_with_mlir(op_graph) + if offending_nodes is not None: + raise RuntimeError( + "function you are trying to compile isn't supported for MLIR lowering\n\n" + + get_printable_graph(op_graph, show_data_types=True, highlighted_nodes=offending_nodes) + ) # Update bit_width for MLIR update_bit_width_for_mlir(op_graph) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 9af843c9d..5a4662b4d 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -560,29 +560,50 @@ def test_compile_function_with_direct_tlu_overflow(): @pytest.mark.parametrize( - "function,input_ranges,list_of_arg_names", + "function,parameters,inputset,match", [ - pytest.param(lambda x: x - 10, ((-5, 5),), ["x"]), + pytest.param( + lambda x: 1 - x, + {"x": EncryptedScalar(Integer(3, is_signed=False))}, + [(i,) for i in range(8)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 + "%1 = x # EncryptedScalar>\n" # noqa: E501 + "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ signed integer outputs aren't supported\n" # noqa: E501 + "return(%2)\n" + ), + ), + pytest.param( + lambda x: x + 1, + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, + [(numpy.random.randint(0, 8, size=(2, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 + "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ non scalar outputs aren't supported\n" # noqa: E501 + "return(%2)\n" + ), + ), ], ) -def test_fail_compile(function, input_ranges, list_of_arg_names): +def test_fail_compile(function, parameters, inputset, match): """Test function compile_numpy_function_into_op_graph for a program with signed values""" - def data_gen(args): - for prod in itertools.product(*args): - yield prod - - function_parameters = { - arg_name: EncryptedScalar(Integer(64, True)) for arg_name in list_of_arg_names - } - - with pytest.raises(RuntimeError, match=".*isn't supported for MLIR lowering.*"): + try: compile_numpy_function( function, - function_parameters, - data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + parameters, + inputset, CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), ) + except RuntimeError as error: + assert str(error) == match def test_small_inputset(): From a811b588c6a249880fcbafc6c1869bfb0334424a Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 10:01:25 +0200 Subject: [PATCH 0422/1104] test: fix ldexp correctness test by changing the input type --- tests/numpy/test_compile.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 5a4662b4d..b2f1aa847 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -164,7 +164,7 @@ def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, i for prod in itertools.product(*args): yield prod - function_parameters = {arg_name: EncryptedScalar(Integer(64, False)) for arg_name in ["x", "y"]} + function_parameters = {arg_name: EncryptedScalar(Integer(64, True)) for arg_name in ["x", "y"]} compiler_engine = compile_numpy_function( function, @@ -222,14 +222,10 @@ def test_binary_ufunc_operations(ufunc): ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) ) elif ufunc in [numpy.ldexp]: - # Can't make it work - # TODO: fixme - pass - # Need small constants to keep results sufficiently small - # subtest_compile_and_run_binary_ufunc_correctness( - # ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) - # ) + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) + ) else: # General case subtest_compile_and_run_binary_ufunc_correctness( From 1c8c6239515e7cd26b877ea6bf1e6f93ab2bc6b9 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 15 Oct 2021 10:37:24 +0200 Subject: [PATCH 0423/1104] test: deal with probabilistic nature of correctness tests closes #656 refs #551 --- tests/numpy/test_compile.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index b2f1aa847..ce02e0841 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -123,6 +123,28 @@ def mix_x_and_y_and_call_binary_f_two_avoid_0_input(func, c, x, y): return z +def check_is_good_execution(compiler_engine, function, args): + """Run several times the check compiler_engine.run(*args) == function(*args). If always wrong, + return an error. One can set the expected probability of success of one execution and the + number of tests, to finetune the probability of bad luck, ie that we run several times the + check and always have a wrong result.""" + expected_probability_of_success = 0.95 + nb_tries = 5 + expected_bad_luck = (1 - expected_probability_of_success) ** nb_tries + + for i in range(1, nb_tries + 1): + if compiler_engine.run(*args) == function(*args): + # Good computation after i tries + print(f"Good computation after {i} tries") + return + + # Bad computation after nb_tries + raise AssertionError( + f"bad computation after {nb_tries} tries, which was supposed to happen with a " + f"probability of {expected_bad_luck}" + ) + + def subtest_compile_and_run_unary_ufunc_correctness(ufunc, upper_function, input_ranges): """Test correctness of results when running a compiled function""" @@ -145,11 +167,7 @@ def subtest_compile_and_run_unary_ufunc_correctness(ufunc, upper_function, input args = [random.randint(low, high) for (low, high) in input_ranges] - # TODO: fix the check - # assert compiler_engine.run(*args) == function(*args) - - if compiler_engine.run(*args) != function(*args): - print("Warning, bad computation") + check_is_good_execution(compiler_engine, function, args) def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, input_ranges): @@ -174,11 +192,7 @@ def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, i args = [random.randint(low, high) for (low, high) in input_ranges] - # TODO: fix the check - # assert compiler_engine.run(*args) == function(*args) - - if compiler_engine.run(*args) != function(*args): - print("Warning, bad computation") + check_is_good_execution(compiler_engine, function, args) @pytest.mark.parametrize( From 2d866ae3c119b08506e798c542d14d6e1dafc3ef Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 15 Oct 2021 13:05:59 +0200 Subject: [PATCH 0424/1104] chore: show types and optional scopes for conventional commits. --- Makefile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Makefile b/Makefile index ddd3d4aa2..1380ea636 100644 --- a/Makefile +++ b/Makefile @@ -238,3 +238,13 @@ changelog: check_version_coherence PROJECT_VER="$${PROJECT_VER[1]}";\ poetry run python ./script/make_utils/changelog_helper.py > "CHANGELOG_$${PROJECT_VER}.md" .PHONY: changelog + +# Show the accepted types and optional scopes +show_scope: + @echo "Accepted types and optional scopes:" + @cat .github/workflows/continuous-integration.yaml | grep feat | grep pattern | cut -f 2- -d ":" | cut -f 2- -d " " +.PHONY: show_scope + +show_type:show_scope +.PHONY: show_type + From 71c794dbeb8cc973919369793a98442d10425b0e Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 15 Oct 2021 14:24:53 +0300 Subject: [PATCH 0425/1104] fix(compilation): remove 7-bit requirement for lookup tables --- concrete/common/mlir/utils.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index ad2e302b8..d5e153419 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -88,13 +88,6 @@ def update_bit_width_for_mlir(op_graph: OPGraph): if current_node_out_bit_width > ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB: offending_list.append((node, current_node_out_bit_width)) - # TODO: remove this workaround, which was for #279, once the compiler can handle - # smaller tables, #412 - has_a_table = any(isinstance(node, UnivariateFunction) for node in op_graph.graph.nodes) - - if has_a_table: - max_bit_width = ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB - _set_all_bit_width(op_graph, max_bit_width) # Check that the max_bit_width is supported by the compiler From 1711663f67e135e15774830a825e2b639ef953db Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 13:06:07 +0200 Subject: [PATCH 0426/1104] refactor: use numpy err context manager in get_numpy_function_output_dtype --- concrete/numpy/np_dtypes_helpers.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 24708bb47..78ac6a9bc 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -205,22 +205,18 @@ def get_numpy_function_output_dtype( input_numpy_dtypes = [convert_base_data_type_to_numpy_dtype(dtype) for dtype in input_dtypes] - # Store numpy old error settings and ignore all errors in this function - # We ignore errors as we may call functions with invalid inputs just to get the proper output - # dtypes - old_numpy_err_settings = numpy.seterr(all="ignore") - dummy_inputs = tuple( dtype.type(1000.0 * numpy.random.random_sample()) for dtype in input_numpy_dtypes ) - outputs = function(*dummy_inputs) + # We ignore errors as we may call functions with invalid inputs just to get the proper output + # dtypes + with numpy.errstate(all="ignore"): + outputs = function(*dummy_inputs) + if not isinstance(outputs, tuple): outputs = (outputs,) - # Restore numpy error settings - numpy.seterr(**old_numpy_err_settings) - return [output.dtype for output in outputs] From 93e39e58f74a5c82f8ea3bab39637ba394ff2480 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 09:33:19 +0200 Subject: [PATCH 0427/1104] fix(representation): handle failure in UnivariateFunction get_table - if only some values are problematic, flood fill the resulting table with other valid values - if no valid value was generated an AssertionError will be thrown --- .../common/representation/intermediate.py | 37 +++++++++++++++++- concrete/numpy/compile.py | 39 +++++++++++-------- .../representation/test_intermediate.py | 22 +++++++++++ tests/numpy/test_compile.py | 17 ++------ 4 files changed, 84 insertions(+), 31 deletions(-) diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index f694d61de..c96029b64 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -1,6 +1,7 @@ """File containing code to represent source programs operations.""" from abc import ABC, abstractmethod +from collections import deque from copy import deepcopy from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type @@ -196,6 +197,31 @@ class Constant(IntermediateNode): return str(self.constant_data) +def flood_replace_none_values(table: list): + """Use a flooding algorithm to replace None values. + + Args: + table (list): the list in which there are None values that need to be replaced by copies of + the closest non None data from the list. + """ + assert_true(any(value is not None for value in table)) + + not_none_values_idx = deque(idx for idx, value in enumerate(table) if value is not None) + while not_none_values_idx: + current_idx = not_none_values_idx.popleft() + current_value = table[current_idx] + previous_idx = current_idx - 1 + next_idx = current_idx + 1 + if previous_idx >= 0 and table[previous_idx] is None: + table[previous_idx] = deepcopy(current_value) + not_none_values_idx.append(previous_idx) + if next_idx < len(table) and table[next_idx] is None: + table[next_idx] = deepcopy(current_value) + not_none_values_idx.append(next_idx) + + assert_true(all(value is not None for value in table)) + + class UnivariateFunction(IntermediateNode): """Node representing an univariate arbitrary function, e.g. sin(x).""" @@ -267,11 +293,20 @@ class UnivariateFunction(IntermediateNode): min_input_range = input_dtype.min_value() max_input_range = input_dtype.max_value() + 1 + def catch(func, *args, **kwargs): + try: + return func(*args, **kwargs) + # We currently cannot trigger exceptions in the code during evaluation + except Exception: # pragma: no cover # pylint: disable=broad-except + return None + table = [ - self.evaluate({0: input_value_constructor(input_value)}) + catch(self.evaluate, {0: input_value_constructor(input_value)}) for input_value in range(min_input_range, max_input_range) ] + flood_replace_none_values(table) + return table diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index fa64e54c3..5604c00f8 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -217,13 +217,15 @@ def compile_numpy_function_into_op_graph( # Try to compile the function and save partial artifacts on failure try: - return _compile_numpy_function_into_op_graph_internal( - function_to_compile, - function_parameters, - inputset, - compilation_configuration, - compilation_artifacts, - ) + # Use context manager to restore numpy error handling + with numpy.errstate(**numpy.geterr()): + return _compile_numpy_function_into_op_graph_internal( + function_to_compile, + function_parameters, + inputset, + compilation_configuration, + compilation_artifacts, + ) except Exception: # pragma: no cover # This branch is reserved for unexpected issues and hence it shouldn't be tested. # If it could be tested, we would have fixed the underlying issue. @@ -280,7 +282,10 @@ def _compile_numpy_function_internal( # Convert graph to an MLIR representation converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) - mlir_result = converter.convert(op_graph) + + # Disable numpy warnings during conversion to avoid issues during TLU generation + with numpy.errstate(all="ignore"): + mlir_result = converter.convert(op_graph) # Show MLIR representation if requested if show_mlir: @@ -337,14 +342,16 @@ def compile_numpy_function( # Try to compile the function and save partial artifacts on failure try: - return _compile_numpy_function_internal( - function_to_compile, - function_parameters, - inputset, - compilation_configuration, - compilation_artifacts, - show_mlir, - ) + # Use context manager to restore numpy error handling + with numpy.errstate(**numpy.geterr()): + return _compile_numpy_function_internal( + function_to_compile, + function_parameters, + inputset, + compilation_configuration, + compilation_artifacts, + show_mlir, + ) except Exception: # pragma: no cover # This branch is reserved for unexpected issues and hence it shouldn't be tested. # If it could be tested, we would have fixed the underlying issue. diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 847a0e562..c20588f20 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -1,5 +1,7 @@ """Test file for intermediate representation""" +from copy import deepcopy + import numpy import pytest @@ -292,3 +294,23 @@ def test_is_equivalent_to( == test_helpers.nodes_are_equivalent(node2, node1) == expected_result ) + + +@pytest.mark.parametrize( + "list_to_fill,expected_list", + [ + pytest.param([None, 1, 2, 3, None, None], [1, 1, 2, 3, 3, 3]), + pytest.param([None], None, marks=pytest.mark.xfail(strict=True)), + pytest.param([None, None, None, None, 7, None, None, None], [7, 7, 7, 7, 7, 7, 7, 7]), + pytest.param([None, None, 3, None, None, None, 2, None], [3, 3, 3, 3, 3, 2, 2, 2]), + ], +) +def test_flood_replace_none_values(list_to_fill: list, expected_list: list): + """Unit test for flood_replace_none_values""" + + # avoid modifying the test input + list_to_fill_copy = deepcopy(list_to_fill) + ir.flood_replace_none_values(list_to_fill_copy) + + assert all(value is not None for value in list_to_fill_copy) + assert list_to_fill_copy == expected_list diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index ce02e0841..6801a9754 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -116,13 +116,6 @@ def mix_x_and_y_and_call_binary_f_two(func, c, x, y): return z -def mix_x_and_y_and_call_binary_f_two_avoid_0_input(func, c, x, y): - """Create an upper function to test `func`""" - z = numpy.abs(func(c, x + 1) + 1) - z = z.astype(numpy.uint32) + y - return z - - def check_is_good_execution(compiler_engine, function, args): """Run several times the check compiler_engine.run(*args) == function(*args). If always wrong, return an error. One can set the expected probability of success of one execution and the @@ -223,13 +216,9 @@ def test_binary_ufunc_operations(ufunc): ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 4), (0, 5)) ) elif ufunc in [numpy.floor_divide, numpy.fmod, numpy.remainder, numpy.true_divide]: - # 0 not in the domain of definition - # Can't make it work, #649 - # TODO: fixme - pass - # subtest_compile_and_run_binary_ufunc_correctness( - # ufunc, mix_x_and_y_and_call_binary_f_two_avoid_0_input, 31, ((1, 5), (1, 5)) - # ) + subtest_compile_and_run_binary_ufunc_correctness( + ufunc, mix_x_and_y_and_call_binary_f_two, 31, ((1, 5), (1, 5)) + ) elif ufunc in [numpy.lcm, numpy.left_shift]: # Need small constants to keep results sufficiently small subtest_compile_and_run_binary_ufunc_correctness( From 2a1eb40bf20c637bae6cfe73f56fd3bbe60cb4a5 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 13:43:25 +0200 Subject: [PATCH 0428/1104] test: use xdist to speed-up testing --- Makefile | 1 + poetry.lock | 85 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1380ea636..f3debc5d9 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,7 @@ pcc_internal: $(PCC_DEPS) pytest: poetry run pytest -svv \ --global-coverage-infos-json=global-coverage-infos.json \ + -n auto \ --cov=$(SRC_DIR) --cov-fail-under=100 \ --randomly-dont-reorganize \ --cov-report=term-missing:skip-covered tests/ diff --git a/poetry.lock b/poetry.lock index 9d9fa4e6c..931d6d1a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -302,6 +302,17 @@ category = "dev" optional = false python-versions = ">=2.7" +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + [[package]] name = "flake8" version = "3.9.2" @@ -1300,6 +1311,18 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-forked" +version = "1.3.0" +description = "run tests in isolated forked subprocesses" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + [[package]] name = "pytest-randomly" version = "3.10.1" @@ -1312,6 +1335,24 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} pytest = "*" +[[package]] +name = "pytest-xdist" +version = "2.4.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.0.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1913,7 +1954,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "ca03765d05748d41d638bffcb0a7fb021b91636884eeb3d76bcd86f0e29a1511" +content-hash = "7ab160d17a98f1b6dd4bf2083cc03875bf3abdde55b14334df3dbf7d1dd2233d" [metadata.files] alabaster = [ @@ -2143,6 +2184,10 @@ entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, @@ -2332,12 +2377,22 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2346,14 +2401,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2363,6 +2425,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2729,10 +2794,18 @@ pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] +pytest-forked = [ + {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, + {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, +] pytest-randomly = [ {file = "pytest-randomly-3.10.1.tar.gz", hash = "sha256:d4ef5dbf27e542e6a4e4cec7a20ef3f1b906bce21fa340ca5657b5326ef23a64"}, {file = "pytest_randomly-3.10.1-py3-none-any.whl", hash = "sha256:d28d490e3a743bdd64c5bc87c5fc182eac966ba6432c6bb6b224e32e76527e9e"}, ] +pytest-xdist = [ + {file = "pytest-xdist-2.4.0.tar.gz", hash = "sha256:89b330316f7fc475f999c81b577c2b926c9569f3d397ae432c0c2e2496d61ff9"}, + {file = "pytest_xdist-2.4.0-py3-none-any.whl", hash = "sha256:7b61ebb46997a0820a263553179d6d1e25a8c50d8a8620cd1aa1e20e3be99168"}, +] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -2813,24 +2886,32 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"}, {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"}, {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"}, {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"}, {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, @@ -2838,6 +2919,8 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"}, {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, diff --git a/pyproject.toml b/pyproject.toml index 18435aa1b..7676ba59e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ python-semantic-release = "7.19.2" semver = "^2.13.0" tomlkit = "^0.7.0" GitPython = "^3.1.24" +pytest-xdist = "^2.4.0" [build-system] requires = ["poetry-core>=1.0.0"] From e16467160869883e2be1cf543d5b316ccecaf6ec Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 15 Oct 2021 14:49:57 +0300 Subject: [PATCH 0429/1104] feat(ci): add 'execution' optional scope --- .github/workflows/continuous-integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 292fcf0a0..2c7bc5abd 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -195,10 +195,10 @@ jobs: if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: - pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation)\))?\:) .+$' + pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation|execution)\))?\:) .+$' flags: 'gs' error: "Your first line has to contain a commit type and scope like \"feat(my_feature): msg\".\ - Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation)\\))?\\:)'" + Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation|execution)\\))?\\:)'" excludeDescription: 'true' # optional: this excludes the description body of a pull request excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request From 8b2efb8869fc1c3a05f4c3e88574f183c282a007 Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 15 Oct 2021 14:50:19 +0300 Subject: [PATCH 0430/1104] test(execution): add table lookup correctness tests --- tests/numpy/test_compile.py | 162 ++++++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 7 deletions(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 6801a9754..dab66562e 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -22,15 +22,101 @@ def no_fuse_unhandled(x, y): return intermediate.astype(numpy.int32) -def lut(x): +def identity_lut_generator(n): """Test lookup table""" - table = LookupTable(list(range(128))) + return lambda x: LookupTable(list(range(2 ** n)))[x] + + +def random_lut_1b(x): + """1-bit random table lookup""" + + # fmt: off + table = LookupTable([10, 12]) + # fmt: on + return table[x] -def small_lut(x): - """Test lookup table with small size and output""" - table = LookupTable(list(range(32))) +def random_lut_2b(x): + """2-bit random table lookup""" + + # fmt: off + table = LookupTable([3, 8, 22, 127]) + # fmt: on + + return table[x] + + +def random_lut_3b(x): + """3-bit random table lookup""" + + # fmt: off + table = LookupTable([30, 52, 125, 23, 17, 12, 90, 4]) + # fmt: on + + return table[x] + + +def random_lut_4b(x): + """4-bit random table lookup""" + + # fmt: off + table = LookupTable([30, 52, 125, 23, 17, 12, 90, 4, 21, 51, 22, 15, 53, 100, 75, 90]) + # fmt: on + + return table[x] + + +def random_lut_5b(x): + """5-bit random table lookup""" + + # fmt: off + table = LookupTable( + [ + 1, 5, 2, 3, 10, 2, 4, 8, 1, 12, 15, 12, 10, 1, 0, 2, + 4, 3, 8, 7, 10, 11, 6, 13, 9, 0, 2, 1, 15, 11, 12, 5 + ] + ) + # fmt: on + + return table[x] + + +def random_lut_6b(x): + """6-bit random table lookup""" + + # fmt: off + table = LookupTable( + [ + 95, 74, 11, 83, 24, 116, 28, 75, 26, 85, 114, 121, 91, 123, 78, 69, + 72, 115, 67, 5, 39, 11, 120, 88, 56, 43, 74, 16, 72, 85, 103, 92, + 44, 115, 50, 56, 107, 77, 25, 71, 52, 45, 80, 35, 69, 8, 40, 87, + 26, 85, 84, 53, 73, 95, 86, 22, 16, 45, 59, 112, 53, 113, 98, 116 + ] + ) + # fmt: on + + return table[x] + + +def random_lut_7b(x): + """7-bit random table lookup""" + + # fmt: off + table = LookupTable( + [ + 13, 58, 38, 58, 15, 15, 77, 86, 80, 94, 108, 27, 126, 60, 65, 95, + 50, 79, 22, 97, 38, 60, 25, 48, 73, 112, 27, 45, 88, 20, 67, 17, + 16, 6, 71, 60, 77, 43, 93, 40, 41, 31, 99, 122, 120, 40, 94, 13, + 111, 44, 96, 62, 108, 91, 34, 90, 103, 58, 3, 103, 19, 69, 55, 108, + 0, 111, 113, 0, 0, 73, 22, 52, 81, 2, 88, 76, 36, 121, 97, 121, + 123, 79, 82, 120, 12, 65, 54, 101, 90, 52, 84, 106, 23, 15, 110, 79, + 85, 101, 30, 61, 104, 35, 81, 30, 98, 44, 111, 32, 68, 18, 45, 123, + 84, 80, 68, 27, 31, 38, 126, 61, 51, 7, 49, 37, 63, 114, 22, 18, + ] + ) + # fmt: on + return table[x] @@ -352,8 +438,20 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n pytest.param(lambda x: x * 2, ((0, 10),), ["x"]), pytest.param(lambda x: 12 - x, ((0, 10),), ["x"]), pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), - pytest.param(lut, ((0, 127),), ["x"]), - pytest.param(small_lut, ((0, 31),), ["x"]), + pytest.param(identity_lut_generator(1), ((0, 1),), ["x"]), + pytest.param(identity_lut_generator(2), ((0, 3),), ["x"]), + pytest.param(identity_lut_generator(3), ((0, 7),), ["x"]), + pytest.param(identity_lut_generator(4), ((0, 15),), ["x"]), + pytest.param(identity_lut_generator(5), ((0, 31),), ["x"]), + pytest.param(identity_lut_generator(6), ((0, 63),), ["x"]), + pytest.param(identity_lut_generator(7), ((0, 127),), ["x"]), + pytest.param(random_lut_1b, ((0, 1),), ["x"]), + pytest.param(random_lut_2b, ((0, 3),), ["x"]), + pytest.param(random_lut_3b, ((0, 7),), ["x"]), + pytest.param(random_lut_4b, ((0, 15),), ["x"]), + pytest.param(random_lut_5b, ((0, 31),), ["x"]), + pytest.param(random_lut_6b, ((0, 63),), ["x"]), + pytest.param(random_lut_7b, ((0, 127),), ["x"]), pytest.param(small_fused_table, ((0, 31),), ["x"]), ], ) @@ -523,6 +621,56 @@ def test_compile_and_run_constant_dot_correctness(size, input_range): assert right_circuit.run(*args) == right(*args) +@pytest.mark.parametrize( + "function,input_ranges,list_of_arg_names", + [ + pytest.param(identity_lut_generator(1), ((0, 1),), ["x"], id="identity function (1-bit)"), + pytest.param(identity_lut_generator(2), ((0, 3),), ["x"], id="identity function (2-bit)"), + pytest.param(identity_lut_generator(3), ((0, 7),), ["x"], id="identity function (3-bit)"), + pytest.param(identity_lut_generator(4), ((0, 15),), ["x"], id="identity function (4-bit)"), + pytest.param(identity_lut_generator(5), ((0, 31),), ["x"], id="identity function (5-bit)"), + pytest.param(identity_lut_generator(6), ((0, 63),), ["x"], id="identity function (6-bit)"), + pytest.param(identity_lut_generator(7), ((0, 127),), ["x"], id="identity function (7-bit)"), + pytest.param(random_lut_1b, ((0, 1),), ["x"], id="random function (1-bit)"), + pytest.param(random_lut_2b, ((0, 3),), ["x"], id="random function (2-bit)"), + pytest.param(random_lut_3b, ((0, 7),), ["x"], id="random function (3-bit)"), + pytest.param(random_lut_4b, ((0, 15),), ["x"], id="random function (4-bit)"), + pytest.param(random_lut_5b, ((0, 31),), ["x"], id="random function (5-bit)"), + pytest.param(random_lut_6b, ((0, 63),), ["x"], id="random function (6-bit)"), + pytest.param(random_lut_7b, ((0, 127),), ["x"], id="random function (7-bit)"), + ], +) +def test_compile_and_run_lut_correctness(function, input_ranges, list_of_arg_names): + """Test correctness of results when running a compiled function with LUT""" + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names + } + + compiler_engine = compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + ) + + # testing random values + for _ in range(10): + args = [random.randint(low, high) for (low, high) in input_ranges] + check_is_good_execution(compiler_engine, function, args) + + # testing low values + args = [low for (low, _) in input_ranges] + check_is_good_execution(compiler_engine, function, args) + + # testing high values + args = [high for (_, high) in input_ranges] + check_is_good_execution(compiler_engine, function, args) + + def test_compile_function_with_direct_tlu(): """Test compile_numpy_function_into_op_graph for a program with direct table lookup""" From d82f71e79a1a570008022d168519c3d54f299860 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 15 Oct 2021 14:02:55 +0200 Subject: [PATCH 0431/1104] chore: more verbosing in case of errors. --- concrete/common/mlir/converters.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 21d09c791..74f08cebb 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -169,9 +169,15 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): assert_true(len(node.inputs) == 1, "LUT should have a single input") assert_true(len(node.outputs) == 1, "LUT should have a single output") if not value_is_encrypted_scalar_unsigned_integer(node.inputs[0]): - raise TypeError("Only support LUT with encrypted unsigned integers inputs") + raise TypeError( + f"Only support LUT with encrypted unsigned integers inputs " + f"(but {node.inputs[0]} is provided)" + ) if not value_is_encrypted_scalar_unsigned_integer(node.outputs[0]): - raise TypeError("Only support LUT with encrypted unsigned integers outputs") + raise TypeError( + f"Only support LUT with encrypted unsigned integers outputs " + f"(but {node.outputs[0]} is provided)" + ) x_node = preds[0] x = ir_to_mlir_node[x_node] From bc2ae7be47f65ca9e1d3f7bd4744bd2d9df7e488 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 14:32:29 +0200 Subject: [PATCH 0432/1104] chore: disable pylint line too long in test_compile where necessary - fix pylint targets for non package dirs --- Makefile | 8 ++++---- tests/__init__.py | 1 - tests/common/optimization/test_float_fusing.py | 12 ++++-------- tests/numpy/test_compile.py | 16 ++++++++-------- 4 files changed, 16 insertions(+), 21 deletions(-) delete mode 100644 tests/__init__.py diff --git a/Makefile b/Makefile index f3debc5d9..eb9c291ee 100644 --- a/Makefile +++ b/Makefile @@ -42,17 +42,17 @@ pylint_src: pylint_tests: @# Disable duplicate code detection in tests - poetry run pylint --disable=R0801 --rcfile=pylintrc tests + find ./tests/ -type f -name "*.py" | xargs poetry run pylint --disable=R0801 --rcfile=pylintrc .PHONY: pylint_tests pylint_benchmarks: @# Disable duplicate code detection, docstring requirement, too many locals/statements - poetry run pylint --disable=R0801,R0914,R0915,C0103,C0114,C0115,C0116 \ - --rcfile=pylintrc benchmarks + find ./benchmarks/ -type f -name "*.py" | xargs poetry run pylint \ + --disable=R0801,R0914,R0915,C0103,C0114,C0115,C0116 --rcfile=pylintrc .PHONY: pylint_benchmarks pylint_script: - poetry run pylint --rcfile=pylintrc script + find ./script/ -type f -name "*.py" | xargs poetry run pylint --rcfile=pylintrc .PHONY: pylint_script flake8: diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 1e38b00cd..000000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test module.""" diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 5192081e3..0b715eef6 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -283,10 +283,9 @@ def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): # Create inputs which are either of the form [x, x] or [x, y] for j in range(4): - if fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan]: - if j > 0: - # Domain of definition for these functions - break + if fun in [numpy.arctanh, numpy.arccos, numpy.arcsin, numpy.arctan] and j > 0: + # Domain of definition for these functions + break input_a = input_ input_b = input_ + j @@ -295,10 +294,7 @@ def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): numpy.random.shuffle(input_a) numpy.random.shuffle(input_b) - if random.randint(0, 1) == 0: - inputs = (input_a, input_b) - else: - inputs = (input_b, input_a) + inputs = (input_a, input_b) if random.randint(0, 1) == 0 else (input_b, input_a) function_result = function_to_trace(*inputs) op_graph_result = op_graph(*inputs) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index dab66562e..6e39e8fa2 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -716,10 +716,10 @@ def test_compile_function_with_direct_tlu_overflow(): ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 - "%1 = x # EncryptedScalar>\n" # noqa: E501 - "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ signed integer outputs aren't supported\n" # noqa: E501 + "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ signed integer outputs aren't supported\n" # noqa: E501 # pylint: disable=line-too-long "return(%2)\n" ), ), @@ -730,10 +730,10 @@ def test_compile_function_with_direct_tlu_overflow(): ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 - "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ non scalar outputs aren't supported\n" # noqa: E501 + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ non scalar outputs aren't supported\n" # noqa: E501 # pylint: disable=line-too-long "return(%2)\n" ), ), From 5bb042bc16f9e8233541b58a6e9ae397a173a188 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 16:31:44 +0200 Subject: [PATCH 0433/1104] chore(ci): prepare docs push for release with dummy steps --- .github/workflows/continuous-integration.yaml | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 2c7bc5abd..c3f06417a 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -456,7 +456,7 @@ jobs: outputs: report: ${{ steps.report.outputs.report || 'Did not run.' }} - name: Prepare docker image release + name: Package and artifacts release runs-on: ubuntu-20.04 env: @@ -506,11 +506,12 @@ jobs: --new-version "${GIT_TAG}" \ --existing-versions $EXISTING_TAGS) - REQUIRES_LATEST_TAG=$(echo "${IS_LATEST_INFO}" | jq -rc '.is_latest') + IS_LATEST=$(echo "${IS_LATEST_INFO}" | jq -rc '.is_latest') + echo "IS_LATEST=${IS_LATEST}" >> "$GITHUB_ENV" IS_PRERELEASE=$(echo "${IS_LATEST_INFO}" | jq -rc '.is_prerelease') echo "IS_PRERELEASE=${IS_PRERELEASE}" >> "$GITHUB_ENV" - if [[ "${REQUIRES_LATEST_TAG}" == "true" ]]; then + if [[ "${IS_LATEST}" == "true" ]]; then RELEASE_IMG_LATEST_TAG="${RELEASE_IMAGE_BASE}:latest" RELEASE_IMG_TAGS_TO_PUSH="${RELEASE_IMG_TAGS_TO_PUSH},${RELEASE_IMG_LATEST_TAG}" fi @@ -536,13 +537,12 @@ jobs: push: false tags: "${{ env.RELEASE_IMG_TAGS_TO_PUSH }}" no-cache: true - - name: Release image sanity check and push + - name: Release image sanity check if: ${{ success() && !cancelled() }} run: | echo "Running sanity check for ${RELEASE_IMG_GIT_TAG}" docker run --rm -v "$(pwd)"/docker/release_resources:/data \ "${RELEASE_IMG_GIT_TAG}" /bin/bash -c "python ./sanity_check.py" - docker image push --all-tags "${RELEASE_IMAGE_BASE}" - name: Create directory for artifacts if: ${{ success() && !cancelled() }} run: | @@ -579,6 +579,21 @@ jobs: popd cp "${RAW_CHANGELOG_DIR}"/* "${ARTIFACTS_PACKAGED_DIR}" ls -a . + - name: Push release docker image + if: ${{ success() && !cancelled() }} + run: | + docker image push --all-tags "${RELEASE_IMAGE_BASE}" + - name: Push release documentation + if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) }} + run: | + echo "Should push release documentation as ${GIT_TAG}" + echo "The dir to push would be: ${{ steps.download-docs.outputs.download-path }}" + echo "It contains:" + ls -la "${{ steps.download-docs.outputs.download-path }}" + - name: Push release documentation as stable + if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} + run: | + echo "Should push release documentation as stable" - name: Create GitHub release if: ${{ success() && !cancelled() }} id: create-release From a0a0c4bce64a11f2288cc2f94a03643f0fb8d716 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 18 Oct 2021 09:36:19 +0200 Subject: [PATCH 0434/1104] chore(ci): add deps optional scope to fix dependabot PRs --- .github/workflows/continuous-integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index c3f06417a..cd6454137 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -195,10 +195,10 @@ jobs: if: ${{ fromJSON(env.IS_PR) && steps.install-deps.outcome == 'success' && !cancelled() }} uses: gsactions/commit-message-checker@f27f413dcf8ebcb469d2ce4ae4e45e131d105de6 with: - pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation|execution)\))?\:) .+$' + pattern: '^((feat|fix|chore|refactor|style|test|docs)(\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation|execution|deps)\))?\:) .+$' flags: 'gs' error: "Your first line has to contain a commit type and scope like \"feat(my_feature): msg\".\ - Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation|execution)\\))?\\:)'" + Pattern: '^((feat|fix|chore|refactor|style|test|docs)(\\((bounds|helpers|data_types|debugging|extensions|fhe_circuit|mlir|graph|optimization|representation|tracing|values|benchmarks|ci|scripts|compilation|execution|deps)\\))?\\:)'" excludeDescription: 'true' # optional: this excludes the description body of a pull request excludeTitle: 'true' # optional: this excludes the title of a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request From 42d4a1b873f87fc8b98cdcb624b8646426e44e88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Oct 2021 08:12:04 +0000 Subject: [PATCH 0435/1104] chore(deps): bump actions/checkout from 2.3.4 to 2.3.5 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f...1e204e9a9253d643386038d443f96446fa156a97) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/continuous-integration.yaml | 10 +++++----- .github/workflows/package-watcher.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index cd6454137..6772c2904 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -45,7 +45,7 @@ jobs: report: ${{ steps.report.outputs.report || 'Did not run.' }} steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 with: fetch-depth: 0 - name: Get changed files @@ -163,7 +163,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} @@ -402,7 +402,7 @@ jobs: PREFLIGHT_IMAGE: ${{ needs.build-preflight-docker.outputs.image }} steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 - name: Login to GitHub Container Registry uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 with: @@ -463,7 +463,7 @@ jobs: RELEASE_IMAGE_BASE: ghcr.io/zama-ai/concretefhe steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 # To be removed once poetry 1.2 is released to manage dependencies with groups - name: Cache Installation Files uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 @@ -643,7 +643,7 @@ jobs: name: Send Slack notification runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 - name: Prepare whole job status if: ${{ always() }} continue-on-error: true diff --git a/.github/workflows/package-watcher.yaml b/.github/workflows/package-watcher.yaml index ef236b758..01de372ec 100644 --- a/.github/workflows/package-watcher.yaml +++ b/.github/workflows/package-watcher.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout Code - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + uses: actions/checkout@1e204e9a9253d643386038d443f96446fa156a97 - name: Compare image timestamps and notify run: | ./script/actions_utils/container_timestamp_check.sh \ From 9571ad8e781016a31720596d51fd04de25e4040d Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 15 Oct 2021 17:08:26 +0200 Subject: [PATCH 0436/1104] chore: add a Makefile target to grep pylintrc notes - add a small helper python script to parse pylintrc --- Makefile | 8 +++++++ script/make_utils/get_pylintrc_notes.py | 30 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 script/make_utils/get_pylintrc_notes.py diff --git a/Makefile b/Makefile index eb9c291ee..7d76b4cab 100644 --- a/Makefile +++ b/Makefile @@ -249,3 +249,11 @@ show_scope: show_type:show_scope .PHONY: show_type +# grep recursively, ignore binary files, print file line, print file name +# exclude dot dirs, exclude pylintrc (would match the notes) +# exclude notebooks (sometimes matches in svg text), match the notes in this directory +todo: + @NOTES_ARGS=$$(poetry run python ./script/make_utils/get_pylintrc_notes.py \ + --pylintrc-path pylintrc);\ + grep -rInH --exclude-dir='.[^.]*' --exclude=pylintrc --exclude='*.ipynb' "$${NOTES_ARGS}" . +.PHONY: todo diff --git a/script/make_utils/get_pylintrc_notes.py b/script/make_utils/get_pylintrc_notes.py new file mode 100644 index 000000000..c108ca93b --- /dev/null +++ b/script/make_utils/get_pylintrc_notes.py @@ -0,0 +1,30 @@ +"""File to get pylintrc notes""" + +import argparse +import configparser +from pathlib import Path + + +def main(args): + """Entry point""" + + pylintrc_file_path = Path(args.pylintrc_path).resolve() + config = configparser.ConfigParser() + config.read(pylintrc_file_path) + notes = sorted(map(lambda x: x.strip(), config["MISCELLANEOUS"]["notes"].split(","))) + # Make sure we at least have todo in there without writing it otherwise we'll match + notes.append("TO" + "DO") + notes_for_grep_search = r"\|".join(notes) + print(notes_for_grep_search) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Parse pylintrc notes", allow_abbrev=False) + + parser.add_argument( + "--pylintrc-path", type=str, required=True, help="Path to pylintrc ini config" + ) + + cli_args = parser.parse_args() + + main(cli_args) From a8aafcb70ad93da9a28cd990de3b336225ee0b12 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 15 Oct 2021 15:40:02 +0200 Subject: [PATCH 0437/1104] docs: autogenerate the list of supported functions closes #410 --- Makefile | 15 ++- .../tutorial/WORKING_WITH_FLOATING_POINTS.md | 114 +++++++++++++----- script/doc_utils/gen_supported_ufuncs.py | 85 +++++++++++++ 3 files changed, 180 insertions(+), 34 deletions(-) create mode 100644 script/doc_utils/gen_supported_ufuncs.py diff --git a/Makefile b/Makefile index 7d76b4cab..d1af79bc2 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ flake8: python_linting: pylint flake8 .PHONY: python_linting -conformance: finalize_nb python_format +conformance: finalize_nb python_format supported_functions .PHONY: conformance pcc: @@ -72,7 +72,7 @@ pcc: .PHONY: pcc PCC_DEPS := check_python_format check_finalize_nb python_linting mypy_ci pydocstyle shell_lint -PCC_DEPS += check_version_coherence +PCC_DEPS += check_version_coherence check_supported_functions pcc_internal: $(PCC_DEPS) .PHONY: pcc_internal @@ -158,7 +158,7 @@ docker_publish_measurements: docker_build /bin/bash ./script/progress_tracker_utils/benchmark_and_publish_findings_in_docker.sh .PHONY: docker_publish_measurements -docs: clean_docs +docs: clean_docs supported_functions @# Generate the auto summary of documentations poetry run sphinx-apidoc -o docs/_apidoc $(SRC_DIR) @@ -257,3 +257,12 @@ todo: --pylintrc-path pylintrc);\ grep -rInH --exclude-dir='.[^.]*' --exclude=pylintrc --exclude='*.ipynb' "$${NOTES_ARGS}" . .PHONY: todo + +# Update docs with supported functions +supported_functions: + poetry run python script/doc_utils/gen_supported_ufuncs.py docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +.PHONY: supported_functions + +check_supported_functions: + poetry run python script/doc_utils/gen_supported_ufuncs.py docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md --check +.PHONY: check_supported_functions diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md index 62d088baf..afaaeb4d4 100644 --- a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -27,37 +27,89 @@ circuit.run(60) == 58 The following operations are supported in the latest release, and we'll add more operations in the upcoming releases. -- np.arccos -- np.arccosh -- np.arcsin -- np.arcsinh -- np.arctan -- np.arctanh -- np.cbrt -- np.ceil -- np.cos -- np.cosh -- np.deg2rad -- np.degrees -- np.exp -- np.exp2 -- np.expm1 -- np.fabs -- np.floor -- np.log -- np.log10 -- np.log1p -- np.log2 -- np.rad2deg -- np.radians -- np.rint -- np.sin -- np.sinh -- np.spacing -- np.sqrt -- np.tan -- np.tanh -- np.trunc + + +List of supported unary functions: +- absolute +- arccos +- arccosh +- arcsin +- arcsinh +- arctan +- arctanh +- cbrt +- ceil +- cos +- cosh +- deg2rad +- degrees +- exp +- exp2 +- expm1 +- fabs +- floor +- invert +- isfinite +- isinf +- isnan +- log +- log10 +- log1p +- log2 +- logical_not +- negative +- positive +- rad2deg +- radians +- reciprocal +- rint +- sign +- signbit +- sin +- sinh +- spacing +- sqrt +- square +- tan +- tanh +- trunc + +List of supported binary functions if one of the two operators is a constant scalar: +- arctan2 +- bitwise_and +- bitwise_or +- bitwise_xor +- copysign +- equal +- float_power +- floor_divide +- fmax +- fmin +- fmod +- gcd +- greater +- greater_equal +- heaviside +- hypot +- lcm +- ldexp +- left_shift +- less +- less_equal +- logaddexp +- logaddexp2 +- logical_and +- logical_or +- logical_xor +- maximum +- minimum +- nextafter +- not_equal +- power +- remainder +- right_shift +- true_divide + ## Limitations diff --git a/script/doc_utils/gen_supported_ufuncs.py b/script/doc_utils/gen_supported_ufuncs.py new file mode 100644 index 000000000..6de3baeaa --- /dev/null +++ b/script/doc_utils/gen_supported_ufuncs.py @@ -0,0 +1,85 @@ +"""Update list of supported functions in the doc.""" +import argparse + +from concrete.numpy import tracing + + +def main(file_to_update): + """Update list of supported functions in file_to_update""" + supported_unary_ufunc = sorted( + f.__name__ for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1 + ) + supported_binary_ufunc = sorted( + f.__name__ for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 2 + ) + + with open(file_to_update, "r", encoding="utf-8") as file: + lines = file.readlines() + + newlines = [] + keep_line = True + + for line in lines: + if line.startswith( + "" + ): + keep_line = False + newlines.append(line) + newlines.append( + "\n" + ) + elif line.startswith( + "" + ): + pass + elif line.startswith( + "" + ): + keep_line = True + + # Inject the supported functions + newlines.append("List of supported unary functions:\n") + + newlines.extend(f"- {f}\n" for f in supported_unary_ufunc) + + newlines.append("\n") + newlines.append( + "List of supported binary functions if one of the " + "two operators is a constant scalar:\n" + ) + + newlines.extend(f"- {f}\n" for f in supported_binary_ufunc) + + newlines.append(line) + else: + assert "gen_supported_ufuncs.py" not in line, ( + f"Error: not expected to have 'gen_supported_ufuncs.py' at line {line} " + f"of {file_to_update}" + ) + + if keep_line: + newlines.append(line) + + if args.check: + + with open(file_to_update, "r", encoding="utf-8") as file: + oldlines = file.readlines() + + assert ( + oldlines == newlines + ), "List of supported functions is not up to date. Please run `make supported_functions`." + + else: + with open(file_to_update, "w", encoding="utf-8") as file: + file.writelines(newlines) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Update list of supported functions in the doc") + parser.add_argument("--check", action="store_true", help="flag to enable just checking mode") + + parser.add_argument("file_to_update", type=str, help=".md file to update") + args = parser.parse_args() + main(args.file_to_update) From 82688206f75933dec590ca67559e5c8672083aad Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 18 Oct 2021 11:02:42 +0200 Subject: [PATCH 0438/1104] refactor(mlir): generate tables before converting nodes to MLIR - MLIRConverter becomes an abstract base class - pass the needed informations in an extra dict to MLIR converters - NPMLIRConverter handles the specifics of numpy tables generation --- concrete/common/mlir/converters.py | 14 ++++----- concrete/common/mlir/mlir_converter.py | 34 ++++++++++++++++++---- concrete/numpy/compile.py | 10 +++---- concrete/numpy/np_mlir_converter.py | 36 ++++++++++++++++++++++++ tests/common/mlir/test_converters.py | 2 ++ tests/common/mlir/test_mlir_converter.py | 21 +++++++------- 6 files changed, 89 insertions(+), 28 deletions(-) create mode 100644 concrete/numpy/np_mlir_converter.py diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 74f08cebb..915b9dc4c 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -26,7 +26,7 @@ from ..representation.intermediate import Add, Constant, Dot, Mul, Sub, Univaria from ..values import TensorValue -def add(node, preds, ir_to_mlir_node, ctx): +def add(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert an addition intermediate node.""" assert_true(len(node.inputs) == 2, "addition should have two inputs") assert_true(len(node.outputs) == 1, "addition should have a single output") @@ -70,7 +70,7 @@ def _add_eint_eint(node, preds, ir_to_mlir_node, ctx): ).result -def sub(node, preds, ir_to_mlir_node, ctx): +def sub(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert a subtraction intermediate node.""" assert_true(len(node.inputs) == 2, "subtraction should have two inputs") assert_true(len(node.outputs) == 1, "subtraction should have a single output") @@ -94,7 +94,7 @@ def _sub_int_eint(node, preds, ir_to_mlir_node, ctx): ).result -def mul(node, preds, ir_to_mlir_node, ctx): +def mul(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert a multiplication intermediate node.""" assert_true(len(node.inputs) == 2, "multiplication should have two inputs") assert_true(len(node.outputs) == 1, "multiplication should have a single output") @@ -123,7 +123,7 @@ def _mul_eint_int(node, preds, ir_to_mlir_node, ctx): ).result -def constant(node, _, __, ctx): +def constant(node, _preds, _ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert a constant input.""" value = node.outputs[0] @@ -164,7 +164,7 @@ def constant(node, _, __, ctx): raise TypeError(f"Don't support {value} constants") -def apply_lut(node, preds, ir_to_mlir_node, ctx): +def apply_lut(node, preds, ir_to_mlir_node, ctx, additional_conversion_info): """Convert a UnivariateFunction intermediate node.""" assert_true(len(node.inputs) == 1, "LUT should have a single input") assert_true(len(node.outputs) == 1, "LUT should have a single output") @@ -181,7 +181,7 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): x_node = preds[0] x = ir_to_mlir_node[x_node] - table = node.get_table() + table = additional_conversion_info["tables"][node] out_dtype = cast(Integer, node.outputs[0].dtype) # Create table dense_elem = DenseElementsAttr.get(np.array(table, dtype=np.uint64), context=ctx) @@ -196,7 +196,7 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx): ).result -def dot(node, preds, ir_to_mlir_node, ctx): +def dot(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert a dot intermediate node.""" assert_true(len(node.inputs) == 2, "Dot should have two inputs") assert_true(len(node.outputs) == 1, "Dot should have a single output") diff --git a/concrete/common/mlir/mlir_converter.py b/concrete/common/mlir/mlir_converter.py index 984fc0fc3..ddb3a8fe2 100644 --- a/concrete/common/mlir/mlir_converter.py +++ b/concrete/common/mlir/mlir_converter.py @@ -1,6 +1,7 @@ """File containing code to convert a DAG containing ir nodes to the compiler opset.""" # pylint: disable=no-name-in-module,no-member -from typing import Tuple, cast +from abc import ABC, abstractmethod +from typing import Any, Dict, Tuple, cast import networkx as nx import zamalang @@ -22,7 +23,7 @@ from ..operator_graph import OPGraph from ..representation.intermediate import Input -class MLIRConverter: +class MLIRConverter(ABC): """Converter of the common IR to MLIR.""" def __init__(self, conversion_functions: dict) -> None: @@ -87,6 +88,18 @@ class MLIRConverter: # unsigned integer are considered signless in the compiler return IntegerType.get_signless(bit_width) + @staticmethod + @abstractmethod + def _generate_additional_info_dict(op_graph: OPGraph) -> Dict[str, Any]: + """Generate the additional_conversion_info dict for the MLIR converter. + + Args: + op_graph (OPGraph): the OPGraph for which we need the conversion infos. + + Returns: + Dict[str, Any]: The dict with the additional conversion infos. + """ + def common_value_to_mlir_type(self, value: values.BaseValue) -> MLIRType: """Convert a common value to its corresponding MLIR Type. @@ -125,11 +138,16 @@ class MLIRConverter: """Convert the graph of IntermediateNode to an MLIR textual representation. Args: - graph: graph of IntermediateNode to be converted + op_graph (OPGraph): graph of IntermediateNode to be converted + + Raises: + NotImplementedError: raised if an unknown node type is encountered. Returns: - textual MLIR representation + str: textual MLIR representation """ + additional_conversion_info = self._generate_additional_info_dict(op_graph) + with self.context, Location.unknown(): module = Module.create() # collect inputs @@ -162,7 +180,13 @@ class MLIRConverter: idx_to_pred[data["input_idx"]] = pred preds = [idx_to_pred[i] for i in range(len(idx_to_pred))] # convert to mlir - result = mlir_op(node, preds, ir_to_mlir_node, self.context) + result = mlir_op( + node, + preds, + ir_to_mlir_node, + self.context, + additional_conversion_info, + ) ir_to_mlir_node[node] = result results = ( diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 5604c00f8..5e754ddfd 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -13,7 +13,7 @@ from ..common.compilation import CompilationArtifacts, CompilationConfiguration from ..common.data_types import Integer from ..common.debugging import get_printable_graph from ..common.fhe_circuit import FHECircuit -from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter +from ..common.mlir import V0_OPSET_CONVERSION_FUNCTIONS from ..common.mlir.utils import ( check_graph_values_compatibility_with_mlir, extend_direct_lookup_tables, @@ -29,6 +29,7 @@ from .np_dtypes_helpers import ( get_base_value_for_numpy_or_python_constant_data, get_constructor_for_numpy_or_python_constant_data, ) +from .np_mlir_converter import NPMLIRConverter def numpy_max_func(lhs: Any, rhs: Any) -> Any: @@ -281,11 +282,8 @@ def _compile_numpy_function_internal( ) # Convert graph to an MLIR representation - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) - - # Disable numpy warnings during conversion to avoid issues during TLU generation - with numpy.errstate(all="ignore"): - mlir_result = converter.convert(op_graph) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + mlir_result = converter.convert(op_graph) # Show MLIR representation if requested if show_mlir: diff --git a/concrete/numpy/np_mlir_converter.py b/concrete/numpy/np_mlir_converter.py new file mode 100644 index 000000000..320a76a4c --- /dev/null +++ b/concrete/numpy/np_mlir_converter.py @@ -0,0 +1,36 @@ +"""Numpy-specific MLIR converter.""" + +from typing import Any, Dict + +import numpy + +from ..common.mlir.mlir_converter import MLIRConverter +from ..common.operator_graph import OPGraph +from ..common.representation.intermediate import UnivariateFunction + + +class NPMLIRConverter(MLIRConverter): + """Numpy-specific MLIR converter.""" + + @staticmethod + def _generate_additional_info_dict(op_graph: OPGraph) -> Dict[str, Any]: + """Generate the additional_conversion_info dict for the MLIR converter. + + Args: + op_graph (OPGraph): the OPGraph for which we need the conversion infos. + + Returns: + Dict[str, Any]: The dict with the additional conversion infos. + """ + + additional_conversion_info = {} + + # Disable numpy warnings during conversion to avoid issues during TLU generation + with numpy.errstate(all="ignore"): + additional_conversion_info["tables"] = { + node: node.get_table() + for node in op_graph.graph.nodes() + if isinstance(node, UnivariateFunction) + } + + return additional_conversion_info diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index 89cfdfd26..d9f590669 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -63,6 +63,7 @@ def test_fail_tlu_input(input_node): [None], None, None, + None, ) @@ -84,4 +85,5 @@ def test_fail_tlu_output(input_node): [None], None, None, + None, ) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index cfb51c91e..3746febee 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -10,10 +10,11 @@ from zamalang.dialects import hlfhe from concrete.common.data_types.integers import Integer from concrete.common.extensions.table import LookupTable -from concrete.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS, MLIRConverter +from concrete.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS from concrete.common.values import ClearScalar, EncryptedScalar from concrete.common.values.tensors import ClearTensor, EncryptedTensor from concrete.numpy.compile import compile_numpy_function_into_op_graph +from concrete.numpy.np_mlir_converter import NPMLIRConverter def add(x, y): @@ -219,7 +220,7 @@ def test_mlir_converter(func, args_dict, args_ranges): """Test the conversion to MLIR by calling the parser from the compiler""" inputset = datagen(*args_ranges) result_graph = compile_numpy_function_into_op_graph(func, args_dict, inputset) - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error compiler.round_trip(mlir_result) @@ -261,7 +262,7 @@ def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): for data in datagen(*args_ranges) ), ) - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error compiler.round_trip(mlir_result) @@ -281,7 +282,7 @@ def test_mlir_converter_dot_vector_and_constant(): {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2,))}, [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], ) - left_converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + left_converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) left_mlir = left_converter.convert(left_graph) right_graph = compile_numpy_function_into_op_graph( @@ -289,7 +290,7 @@ def test_mlir_converter_dot_vector_and_constant(): {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2,))}, [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], ) - right_converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + right_converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) right_mlir = right_converter.convert(right_graph) # testing that this doesn't raise an error @@ -300,7 +301,7 @@ def test_mlir_converter_dot_vector_and_constant(): def test_concrete_encrypted_integer_to_mlir_type(): """Test conversion of EncryptedScalar into MLIR""" value = EncryptedScalar(Integer(7, is_signed=False)) - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) eint = converter.common_value_to_mlir_type(value) assert eint == hlfhe.EncryptedIntegerType.get(converter.context, 7) @@ -309,7 +310,7 @@ def test_concrete_encrypted_integer_to_mlir_type(): def test_concrete_clear_integer_to_mlir_type(is_signed): """Test conversion of ClearScalar into MLIR""" value = ClearScalar(Integer(5, is_signed=is_signed)) - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with converter.context: int_mlir = converter.common_value_to_mlir_type(value) if is_signed: @@ -330,7 +331,7 @@ def test_concrete_clear_integer_to_mlir_type(is_signed): def test_concrete_clear_tensor_integer_to_mlir_type(is_signed, shape): """Test conversion of ClearTensor into MLIR""" value = ClearTensor(Integer(5, is_signed=is_signed), shape) - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with converter.context, Location.unknown(): tensor_mlir = converter.common_value_to_mlir_type(value) if is_signed: @@ -355,7 +356,7 @@ def test_concrete_clear_tensor_integer_to_mlir_type(is_signed, shape): def test_concrete_encrypted_tensor_integer_to_mlir_type(shape): """Test conversion of EncryptedTensor into MLIR""" value = EncryptedTensor(Integer(6, is_signed=False), shape) - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with converter.context, Location.unknown(): tensor_mlir = converter.common_value_to_mlir_type(value) element_type = hlfhe.EncryptedIntegerType.get(converter.context, 6) @@ -369,7 +370,7 @@ def test_concrete_encrypted_tensor_integer_to_mlir_type(shape): def test_failing_concrete_to_mlir_type(): """Test failing conversion of an unsupported type into MLIR""" value = "random" - converter = MLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) + converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) with pytest.raises(TypeError, match=r"can't convert value of type .* to MLIR type"): converter.common_value_to_mlir_type(value) From 384026364ecdbdbd472ca3b4e22f42396818cbd2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 18 Oct 2021 16:09:11 +0200 Subject: [PATCH 0439/1104] test: create default_compilation_configuration fixture - update test code and use it where appropriate - remove duplicate tests that lacked correctness verification --- tests/common/compilation/test_artifacts.py | 3 +- .../common/compilation/test_configuration.py | 7 +- tests/common/debugging/test_drawing.py | 3 +- tests/common/debugging/test_printing.py | 3 +- tests/common/mlir/test_mlir_converter.py | 18 +- tests/common/test_fhe_circuit.py | 12 +- tests/conftest.py | 10 + tests/numpy/test_compile.py | 230 +++++++++++------- 8 files changed, 178 insertions(+), 108 deletions(-) diff --git a/tests/common/compilation/test_artifacts.py b/tests/common/compilation/test_artifacts.py index 59230c92d..bf48a1c42 100644 --- a/tests/common/compilation/test_artifacts.py +++ b/tests/common/compilation/test_artifacts.py @@ -9,7 +9,7 @@ from concrete.common.values import EncryptedScalar from concrete.numpy.compile import compile_numpy_function -def test_artifacts_export(): +def test_artifacts_export(default_compilation_configuration): """Test function to check exporting compilation artifacts""" def function(x): @@ -23,6 +23,7 @@ def test_artifacts_export(): function, {"x": EncryptedScalar(UnsignedInteger(7))}, [(i,) for i in range(10)], + default_compilation_configuration, compilation_artifacts=artifacts, ) diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index 24ca8db5d..daadbd307 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -40,7 +40,9 @@ def simple_fuse_not_output(x): ), ], ) -def test_enable_topological_optimizations(test_helpers, function_to_trace, fused): +def test_enable_topological_optimizations( + test_helpers, function_to_trace, fused, default_compilation_configuration +): """Test function for enable_topological_optimizations flag of compilation configuration""" op_graph = compile_numpy_function_into_op_graph( @@ -50,7 +52,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused for param in signature(function_to_trace).parameters.keys() }, [(i,) for i in range(10)], - CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), + default_compilation_configuration, ) op_graph_not_optimized = compile_numpy_function_into_op_graph( function_to_trace, @@ -62,6 +64,7 @@ def test_enable_topological_optimizations(test_helpers, function_to_trace, fused CompilationConfiguration( dump_artifacts_on_unexpected_failures=False, enable_topological_optimizations=False, + treat_warnings_as_errors=True, ), ) diff --git a/tests/common/debugging/test_drawing.py b/tests/common/debugging/test_drawing.py index 7d5fced48..eb8d3d16f 100644 --- a/tests/common/debugging/test_drawing.py +++ b/tests/common/debugging/test_drawing.py @@ -9,7 +9,7 @@ from concrete.common.values import EncryptedScalar from concrete.numpy.compile import compile_numpy_function_into_op_graph -def test_draw_graph_with_saving(): +def test_draw_graph_with_saving(default_compilation_configuration): """Tests drawing and saving a graph""" def function(x): @@ -19,6 +19,7 @@ def test_draw_graph_with_saving(): function, {"x": EncryptedScalar(Integer(7, True))}, [(i,) for i in range(-5, 5)], + default_compilation_configuration, ) with tempfile.TemporaryDirectory() as tmp: diff --git a/tests/common/debugging/test_printing.py b/tests/common/debugging/test_printing.py index d183329ca..e0bb62c49 100644 --- a/tests/common/debugging/test_printing.py +++ b/tests/common/debugging/test_printing.py @@ -6,7 +6,7 @@ from concrete.common.values import EncryptedScalar from concrete.numpy.compile import compile_numpy_function_into_op_graph -def test_get_printable_graph_with_offending_nodes(): +def test_get_printable_graph_with_offending_nodes(default_compilation_configuration): """Test get_printable_graph with offending nodes""" def function(x): @@ -16,6 +16,7 @@ def test_get_printable_graph_with_offending_nodes(): function, {"x": EncryptedScalar(Integer(7, True))}, [(i,) for i in range(-5, 5)], + default_compilation_configuration, ) highlighted_nodes = {opgraph.input_nodes[0]: "foo"} diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index 3746febee..bb405a928 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -216,10 +216,15 @@ def datagen(*args): ), ], ) -def test_mlir_converter(func, args_dict, args_ranges): +def test_mlir_converter(func, args_dict, args_ranges, default_compilation_configuration): """Test the conversion to MLIR by calling the parser from the compiler""" inputset = datagen(*args_ranges) - result_graph = compile_numpy_function_into_op_graph(func, args_dict, inputset) + result_graph = compile_numpy_function_into_op_graph( + func, + args_dict, + inputset, + default_compilation_configuration, + ) converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error @@ -247,7 +252,9 @@ def test_mlir_converter(func, args_dict, args_ranges): ), ], ) -def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): +def test_mlir_converter_dot_between_vectors( + func, args_dict, args_ranges, default_compilation_configuration +): """Test the conversion to MLIR by calling the parser from the compiler""" assert len(args_dict["x"].shape) == 1 assert len(args_dict["y"].shape) == 1 @@ -261,6 +268,7 @@ def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): (numpy.array([data[0]] * n), numpy.array([data[1]] * n)) for data in datagen(*args_ranges) ), + default_compilation_configuration, ) converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) @@ -268,7 +276,7 @@ def test_mlir_converter_dot_between_vectors(func, args_dict, args_ranges): compiler.round_trip(mlir_result) -def test_mlir_converter_dot_vector_and_constant(): +def test_mlir_converter_dot_vector_and_constant(default_compilation_configuration): """Test the conversion to MLIR by calling the parser from the compiler""" def left_dot_with_constant(x): @@ -281,6 +289,7 @@ def test_mlir_converter_dot_vector_and_constant(): left_dot_with_constant, {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2,))}, [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], + default_compilation_configuration, ) left_converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) left_mlir = left_converter.convert(left_graph) @@ -289,6 +298,7 @@ def test_mlir_converter_dot_vector_and_constant(): right_dot_with_constant, {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2,))}, [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], + default_compilation_configuration, ) right_converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) right_mlir = right_converter.convert(right_graph) diff --git a/tests/common/test_fhe_circuit.py b/tests/common/test_fhe_circuit.py index 42e4d3bab..72145026b 100644 --- a/tests/common/test_fhe_circuit.py +++ b/tests/common/test_fhe_circuit.py @@ -6,7 +6,7 @@ import concrete.numpy as hnp from concrete.common.debugging import draw_graph, get_printable_graph -def test_circuit_str(): +def test_circuit_str(default_compilation_configuration): """Test function for `__str__` method of `Circuit`""" def f(x): @@ -15,12 +15,12 @@ def test_circuit_str(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) inputset = [(i,) for i in range(2 ** 3)] - circuit = hnp.compile_numpy_function(f, {"x": x}, inputset) + circuit = hnp.compile_numpy_function(f, {"x": x}, inputset, default_compilation_configuration) assert str(circuit) == get_printable_graph(circuit.opgraph, show_data_types=True) -def test_circuit_draw(): +def test_circuit_draw(default_compilation_configuration): """Test function for `draw` method of `Circuit`""" def f(x): @@ -29,13 +29,13 @@ def test_circuit_draw(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) inputset = [(i,) for i in range(2 ** 3)] - circuit = hnp.compile_numpy_function(f, {"x": x}, inputset) + circuit = hnp.compile_numpy_function(f, {"x": x}, inputset, default_compilation_configuration) assert filecmp.cmp(circuit.draw(), draw_graph(circuit.opgraph)) assert filecmp.cmp(circuit.draw(vertical=False), draw_graph(circuit.opgraph, vertical=False)) -def test_circuit_run(): +def test_circuit_run(default_compilation_configuration): """Test function for `run` method of `Circuit`""" def f(x): @@ -44,7 +44,7 @@ def test_circuit_run(): x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) inputset = [(i,) for i in range(2 ** 3)] - circuit = hnp.compile_numpy_function(f, {"x": x}, inputset) + circuit = hnp.compile_numpy_function(f, {"x": x}, inputset, default_compilation_configuration) for x in inputset: assert circuit.run(*x) == circuit.engine.run(*x) diff --git a/tests/conftest.py b/tests/conftest.py index 3a4dc94a6..aca5efd9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ import networkx as nx import networkx.algorithms.isomorphism as iso import pytest +from concrete.common.compilation import CompilationConfiguration from concrete.common.representation.intermediate import ( ALL_IR_NODES, Add, @@ -228,3 +229,12 @@ class TestHelpers: def test_helpers(): """Fixture to return the static helper class""" return TestHelpers + + +@pytest.fixture +def default_compilation_configuration(): + """Return the default test compilation configuration""" + return CompilationConfiguration( + dump_artifacts_on_unexpected_failures=False, + treat_warnings_as_errors=True, + ) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 6e39e8fa2..8bc025f86 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -224,7 +224,12 @@ def check_is_good_execution(compiler_engine, function, args): ) -def subtest_compile_and_run_unary_ufunc_correctness(ufunc, upper_function, input_ranges): +def subtest_compile_and_run_unary_ufunc_correctness( + ufunc, + upper_function, + input_ranges, + default_compilation_configuration, +): """Test correctness of results when running a compiled function""" def get_function(ufunc, upper_function): @@ -242,6 +247,7 @@ def subtest_compile_and_run_unary_ufunc_correctness(ufunc, upper_function, input function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, ) args = [random.randint(low, high) for (low, high) in input_ranges] @@ -249,7 +255,13 @@ def subtest_compile_and_run_unary_ufunc_correctness(ufunc, upper_function, input check_is_good_execution(compiler_engine, function, args) -def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, input_ranges): +def subtest_compile_and_run_binary_ufunc_correctness( + ufunc, + upper_function, + c, + input_ranges, + default_compilation_configuration, +): """Test correctness of results when running a compiled function""" def get_function(ufunc, upper_function): @@ -267,6 +279,7 @@ def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, i function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, ) args = [random.randint(low, high) for (low, high) in input_ranges] @@ -278,54 +291,86 @@ def subtest_compile_and_run_binary_ufunc_correctness(ufunc, upper_function, c, i "ufunc", [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 2], ) -def test_binary_ufunc_operations(ufunc): +def test_binary_ufunc_operations(ufunc, default_compilation_configuration): """Test biary functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" if ufunc in [numpy.power, numpy.float_power]: # Need small constants to keep results really small subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_one, 3, ((0, 4), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_one, + 3, + ((0, 4), (0, 5)), + default_compilation_configuration, ) elif ufunc in [numpy.lcm, numpy.left_shift]: # Need small constants to keep results sufficiently small subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_one, 3, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_one, + 3, + ((0, 5), (0, 5)), + default_compilation_configuration, ) else: # General case subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_one, 41, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_one, + 41, + ((0, 5), (0, 5)), + default_compilation_configuration, ) if ufunc in [numpy.power, numpy.float_power]: # Need small constants to keep results really small subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 4), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_two, + 2, + ((0, 4), (0, 5)), + default_compilation_configuration, ) elif ufunc in [numpy.floor_divide, numpy.fmod, numpy.remainder, numpy.true_divide]: subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_two, 31, ((1, 5), (1, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_two, + 31, + ((1, 5), (1, 5)), + default_compilation_configuration, ) elif ufunc in [numpy.lcm, numpy.left_shift]: # Need small constants to keep results sufficiently small subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_two, + 2, + ((0, 5), (0, 5)), + default_compilation_configuration, ) elif ufunc in [numpy.ldexp]: # Need small constants to keep results sufficiently small subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_two, 2, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_two, + 2, + ((0, 5), (0, 5)), + default_compilation_configuration, ) else: # General case subtest_compile_and_run_binary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_binary_f_two, 42, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_binary_f_two, + 42, + ((0, 5), (0, 5)), + default_compilation_configuration, ) @pytest.mark.parametrize( "ufunc", [f for f in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC if f.nin == 1] ) -def test_unary_ufunc_operations(ufunc): +def test_unary_ufunc_operations(ufunc, default_compilation_configuration): """Test unary functions which are in tracing.NPTracer.LIST_OF_SUPPORTED_UFUNC.""" if ufunc in [ numpy.degrees, @@ -333,14 +378,20 @@ def test_unary_ufunc_operations(ufunc): ]: # Need to reduce the output value, to avoid to need too much precision subtest_compile_and_run_unary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_f_which_has_large_outputs, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_f_which_has_large_outputs, + ((0, 5), (0, 5)), + default_compilation_configuration, ) elif ufunc in [ numpy.negative, ]: # Need to turn the input into a float subtest_compile_and_run_unary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_f_with_float_inputs, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_f_with_float_inputs, + ((0, 5), (0, 5)), + default_compilation_configuration, ) elif ufunc in [ numpy.invert, @@ -360,7 +411,10 @@ def test_unary_ufunc_operations(ufunc): ]: # No 0 in the domain of definition subtest_compile_and_run_unary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_f_avoid_0_input, ((1, 5), (1, 5)) + ufunc, + mix_x_and_y_and_call_f_avoid_0_input, + ((1, 5), (1, 5)), + default_compilation_configuration, ) elif ufunc in [ numpy.cosh, @@ -375,12 +429,18 @@ def test_unary_ufunc_operations(ufunc): ]: # Need a small range of inputs, to avoid to need too much precision subtest_compile_and_run_unary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_f_which_expects_small_inputs, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_f_which_expects_small_inputs, + ((0, 5), (0, 5)), + default_compilation_configuration, ) else: # Regular case for univariate functions subtest_compile_and_run_unary_ufunc_correctness( - ufunc, mix_x_and_y_and_call_f, ((0, 5), (0, 5)) + ufunc, + mix_x_and_y_and_call_f, + ((0, 5), (0, 5)), + default_compilation_configuration, ) @@ -404,7 +464,9 @@ def test_unary_ufunc_operations(ufunc): pytest.param(complicated_topology, ((0, 10),), ["x"]), ], ) -def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_names): +def test_compile_function_multiple_outputs( + function, input_ranges, list_of_arg_names, default_compilation_configuration +): """Test function compile_numpy_function_into_op_graph for a program with multiple outputs""" def data_gen(args): @@ -419,7 +481,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), - CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), + default_compilation_configuration, ) # TODO: For the moment, we don't have really checks, but some printfs. Later, @@ -433,52 +495,7 @@ def test_compile_function_multiple_outputs(function, input_ranges, list_of_arg_n @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ - pytest.param(lambda x: x + 42, ((0, 10),), ["x"]), pytest.param(lambda x: x + numpy.int32(42), ((0, 10),), ["x"]), - pytest.param(lambda x: x * 2, ((0, 10),), ["x"]), - pytest.param(lambda x: 12 - x, ((0, 10),), ["x"]), - pytest.param(lambda x, y: x + y + 8, ((2, 10), (4, 8)), ["x", "y"]), - pytest.param(identity_lut_generator(1), ((0, 1),), ["x"]), - pytest.param(identity_lut_generator(2), ((0, 3),), ["x"]), - pytest.param(identity_lut_generator(3), ((0, 7),), ["x"]), - pytest.param(identity_lut_generator(4), ((0, 15),), ["x"]), - pytest.param(identity_lut_generator(5), ((0, 31),), ["x"]), - pytest.param(identity_lut_generator(6), ((0, 63),), ["x"]), - pytest.param(identity_lut_generator(7), ((0, 127),), ["x"]), - pytest.param(random_lut_1b, ((0, 1),), ["x"]), - pytest.param(random_lut_2b, ((0, 3),), ["x"]), - pytest.param(random_lut_3b, ((0, 7),), ["x"]), - pytest.param(random_lut_4b, ((0, 15),), ["x"]), - pytest.param(random_lut_5b, ((0, 31),), ["x"]), - pytest.param(random_lut_6b, ((0, 63),), ["x"]), - pytest.param(random_lut_7b, ((0, 127),), ["x"]), - pytest.param(small_fused_table, ((0, 31),), ["x"]), - ], -) -def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_of_arg_names): - """Test function compile_numpy_function for a program with multiple outputs""" - - def data_gen(args): - for prod in itertools.product(*args): - yield prod - - function_parameters = { - arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names - } - - compiler_engine = compile_numpy_function( - function, - function_parameters, - data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), - ) - - args = [random.randint(low, high) for (low, high) in input_ranges] - compiler_engine.run(*args) - - -@pytest.mark.parametrize( - "function,input_ranges,list_of_arg_names", - [ pytest.param(lambda x: x + 64, ((0, 10),), ["x"]), pytest.param(lambda x: x * 3, ((0, 40),), ["x"]), pytest.param(lambda x: 120 - x, ((40, 80),), ["x"]), @@ -487,7 +504,9 @@ def test_compile_and_run_function_multiple_outputs(function, input_ranges, list_ pytest.param(lambda x, y: 50 - y * 2 + x, ((0, 20), (0, 20)), ["x", "y"]), ], ) -def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): +def test_compile_and_run_correctness( + function, input_ranges, list_of_arg_names, default_compilation_configuration +): """Test correctness of results when running a compiled function""" def data_gen(args): @@ -502,6 +521,7 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, ) args = [random.randint(low, high) for (low, high) in input_ranges] @@ -529,7 +549,7 @@ def test_compile_and_run_correctness(function, input_ranges, list_of_arg_names): ), ], ) -def test_compile_and_run_dot_correctness(size, input_range): +def test_compile_and_run_dot_correctness(size, input_range, default_compilation_configuration): """Test correctness of results when running a compiled function""" low, high = input_range @@ -557,6 +577,7 @@ def test_compile_and_run_dot_correctness(size, input_range): function, function_parameters, inputset, + default_compilation_configuration, ) args = [[random.randint(low, high) for _ in range(size)] for __ in range(2)] @@ -584,7 +605,9 @@ def test_compile_and_run_dot_correctness(size, input_range): ), ], ) -def test_compile_and_run_constant_dot_correctness(size, input_range): +def test_compile_and_run_constant_dot_correctness( + size, input_range, default_compilation_configuration +): """Test correctness of results when running a compiled function""" low, high = input_range @@ -609,11 +632,13 @@ def test_compile_and_run_constant_dot_correctness(size, input_range): left, {"x": EncryptedTensor(Integer(64, False), shape)}, inputset, + default_compilation_configuration, ) right_circuit = compile_numpy_function( left, {"x": EncryptedTensor(Integer(64, False), shape)}, inputset, + default_compilation_configuration, ) args = (numpy.random.randint(low, high + 1, size=shape).tolist(),) @@ -622,39 +647,49 @@ def test_compile_and_run_constant_dot_correctness(size, input_range): @pytest.mark.parametrize( - "function,input_ranges,list_of_arg_names", + "function,input_bits,list_of_arg_names", [ - pytest.param(identity_lut_generator(1), ((0, 1),), ["x"], id="identity function (1-bit)"), - pytest.param(identity_lut_generator(2), ((0, 3),), ["x"], id="identity function (2-bit)"), - pytest.param(identity_lut_generator(3), ((0, 7),), ["x"], id="identity function (3-bit)"), - pytest.param(identity_lut_generator(4), ((0, 15),), ["x"], id="identity function (4-bit)"), - pytest.param(identity_lut_generator(5), ((0, 31),), ["x"], id="identity function (5-bit)"), - pytest.param(identity_lut_generator(6), ((0, 63),), ["x"], id="identity function (6-bit)"), - pytest.param(identity_lut_generator(7), ((0, 127),), ["x"], id="identity function (7-bit)"), - pytest.param(random_lut_1b, ((0, 1),), ["x"], id="random function (1-bit)"), - pytest.param(random_lut_2b, ((0, 3),), ["x"], id="random function (2-bit)"), - pytest.param(random_lut_3b, ((0, 7),), ["x"], id="random function (3-bit)"), - pytest.param(random_lut_4b, ((0, 15),), ["x"], id="random function (4-bit)"), - pytest.param(random_lut_5b, ((0, 31),), ["x"], id="random function (5-bit)"), - pytest.param(random_lut_6b, ((0, 63),), ["x"], id="random function (6-bit)"), - pytest.param(random_lut_7b, ((0, 127),), ["x"], id="random function (7-bit)"), + pytest.param(identity_lut_generator(1), (1,), ["x"], id="identity function (1-bit)"), + pytest.param(identity_lut_generator(2), (2,), ["x"], id="identity function (2-bit)"), + pytest.param(identity_lut_generator(3), (3,), ["x"], id="identity function (3-bit)"), + pytest.param(identity_lut_generator(4), (4,), ["x"], id="identity function (4-bit)"), + pytest.param(identity_lut_generator(5), (5,), ["x"], id="identity function (5-bit)"), + pytest.param(identity_lut_generator(6), (6,), ["x"], id="identity function (6-bit)"), + pytest.param(identity_lut_generator(7), (7,), ["x"], id="identity function (7-bit)"), + pytest.param(random_lut_1b, (1,), ["x"], id="random function (1-bit)"), + pytest.param(random_lut_2b, (2,), ["x"], id="random function (2-bit)"), + pytest.param(random_lut_3b, (3,), ["x"], id="random function (3-bit)"), + pytest.param(random_lut_4b, (4,), ["x"], id="random function (4-bit)"), + pytest.param(random_lut_5b, (5,), ["x"], id="random function (5-bit)"), + pytest.param(random_lut_6b, (6,), ["x"], id="random function (6-bit)"), + pytest.param(random_lut_7b, (7,), ["x"], id="random function (7-bit)"), + pytest.param(small_fused_table, (5,), ["x"], id="small fused table (5-bits)"), ], ) -def test_compile_and_run_lut_correctness(function, input_ranges, list_of_arg_names): +def test_compile_and_run_lut_correctness( + function, + input_bits, + list_of_arg_names, + default_compilation_configuration, +): """Test correctness of results when running a compiled function with LUT""" + input_ranges = tuple((0, 2 ** input_bit - 1) for input_bit in input_bits) + def data_gen(args): for prod in itertools.product(*args): yield prod function_parameters = { - arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names + arg_name: EncryptedScalar(Integer(input_bit, False)) + for input_bit, arg_name in zip(input_bits, list_of_arg_names) } compiler_engine = compile_numpy_function( function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, ) # testing random values @@ -671,7 +706,7 @@ def test_compile_and_run_lut_correctness(function, input_ranges, list_of_arg_nam check_is_good_execution(compiler_engine, function, args) -def test_compile_function_with_direct_tlu(): +def test_compile_function_with_direct_tlu(default_compilation_configuration): """Test compile_numpy_function_into_op_graph for a program with direct table lookup""" table = LookupTable([9, 2, 4, 11]) @@ -683,13 +718,14 @@ def test_compile_function_with_direct_tlu(): function, {"x": EncryptedScalar(Integer(2, is_signed=False))}, [(0,), (1,), (2,), (3,)], + default_compilation_configuration, ) str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) print(f"\n{str_of_the_graph}\n") -def test_compile_function_with_direct_tlu_overflow(): +def test_compile_function_with_direct_tlu_overflow(default_compilation_configuration): """Test compile_numpy_function_into_op_graph for a program with direct table lookup overflow""" table = LookupTable([9, 2, 4, 11]) @@ -702,7 +738,7 @@ def test_compile_function_with_direct_tlu_overflow(): function, {"x": EncryptedScalar(Integer(3, is_signed=False))}, [(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)], - CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), + default_compilation_configuration, ) @@ -739,7 +775,7 @@ def test_compile_function_with_direct_tlu_overflow(): ), ], ) -def test_fail_compile(function, parameters, inputset, match): +def test_fail_compile(function, parameters, inputset, match, default_compilation_configuration): """Test function compile_numpy_function_into_op_graph for a program with signed values""" try: @@ -747,13 +783,13 @@ def test_fail_compile(function, parameters, inputset, match): function, parameters, inputset, - CompilationConfiguration(dump_artifacts_on_unexpected_failures=False), + default_compilation_configuration, ) except RuntimeError as error: assert str(error) == match -def test_small_inputset(): +def test_small_inputset_no_fail(): """Test function compile_numpy_function_into_op_graph with an unacceptably small inputset""" compile_numpy_function_into_op_graph( lambda x: x + 42, @@ -801,7 +837,9 @@ def test_small_inputset_treat_warnings_as_errors(): # pylint: enable=unnecessary-lambda ], ) -def test_compile_function_with_dot(function, params, shape, ref_graph_str): +def test_compile_function_with_dot( + function, params, shape, ref_graph_str, default_compilation_configuration +): """Test compile_numpy_function_into_op_graph for a program with np.dot""" # This is the exhaust, but if ever we have too long inputs (ie, large 'repeat'), @@ -820,6 +858,7 @@ def test_compile_function_with_dot(function, params, shape, ref_graph_str): function, params, data_gen(max_for_ij, repeat), + default_compilation_configuration, ) str_of_the_graph = get_printable_graph(op_graph, show_data_types=True) assert str_of_the_graph == ref_graph_str, ( @@ -840,7 +879,9 @@ def test_compile_function_with_dot(function, params, shape, ref_graph_str): pytest.param(lambda x, y: 50 - y * 2 + x, ((0, 20), (0, 20)), ["x", "y"]), ], ) -def test_compile_with_show_mlir(function, input_ranges, list_of_arg_names): +def test_compile_with_show_mlir( + function, input_ranges, list_of_arg_names, default_compilation_configuration +): """Test show_mlir option""" def data_gen(args): @@ -855,11 +896,12 @@ def test_compile_with_show_mlir(function, input_ranges, list_of_arg_names): function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, show_mlir=True, ) -def test_compile_too_high_bitwidth(): +def test_compile_too_high_bitwidth(default_compilation_configuration): """Check that the check of maximal bitwidth of intermediate data works fine.""" def function(x, y): @@ -882,6 +924,7 @@ def test_compile_too_high_bitwidth(): function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, ) assert ( @@ -898,4 +941,5 @@ def test_compile_too_high_bitwidth(): function, function_parameters, data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, ) From fb0564eea22c22d447bcdf99241b325c2401c549 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 18 Oct 2021 17:22:21 +0200 Subject: [PATCH 0440/1104] docs(ci): change push target for documentation when pushing to main - use preprod bucket through secrets refs #454 --- .github/workflows/continuous-integration.yaml | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 6772c2904..8cdb49849 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -334,7 +334,21 @@ jobs: if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} steps: + - name: Prepare docs push + id: docs-push-infos + run: | + if [[ ${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_BUCKET_NAME }} != "" ]] && \ + [[ ${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_DISTRIBUTION_ID }} != "" ]]; then + REF_NAME=$(echo "${{ github.ref }}" | sed 's/refs\/heads\///g') + echo "::set-output name=has-preprod::true" + echo "::set-output name=aws-bucket::${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_BUCKET_NAME }}" + echo "::set-output name=aws-distribution::${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_DISTRIBUTION_ID }}" + echo "::set-output name=dest-dir::concretefhe/${REF_NAME}" + else + echo "::set-output name=has-preprod::false" + fi - name: Download Documentation + if: ${{ fromJSON(steps.docs-push-infos.outputs.has-preprod) }} id: download uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60 with: @@ -347,28 +361,29 @@ jobs: with: args: --delete --acl public-read env: - AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} + AWS_S3_BUCKET: ${{ steps.docs-push-infos.outputs.aws-bucket }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ secrets.AWS_REGION }} SOURCE_DIR: '.' - DEST_DIR: 'concretefhe' + DEST_DIR: ${{ steps.docs-push-infos.outputs.dest-dir }} - name: Invalidate CloudFront Cache if: ${{ steps.publish.outcome == 'success' }} uses: awact/cloudfront-action@8bcfabc7b4bbc0cb8e55e48527f0e3a6d681627c env: - SOURCE_PATH: '/concretefhe/*' + SOURCE_PATH: "/${{ steps.docs-push-infos.outputs.dest-dir }}/*" AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - DISTRIBUTION_ID: ${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }} + DISTRIBUTION_ID: ${{ steps.docs-push-infos.outputs.aws-distribution }} - name: Set notification report id: report if: ${{ always() }} run: | - REPORT="Publishing documentation finished with status ${{ job.status }}." + REPORT="Publishing documentation finished with status ${{ job.status }}. \ + Pushed to preprod: ${{ steps.docs-push-infos.outputs.has-preprod }}" echo "${REPORT}" echo "::set-output name=report::${REPORT}" echo "REPORT=${REPORT}" >> "$GITHUB_ENV" From 7bf2f096155062ac7920041a802c507a48da481e Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Mon, 18 Oct 2021 11:53:02 +0200 Subject: [PATCH 0441/1104] feat: remove support for np.invert remove support for np.invert and propose to the user to use bitwise_xor instead, because of impossibilities with float fusing closes #658 --- concrete/numpy/tracing.py | 13 +++++++- .../tutorial/WORKING_WITH_FLOATING_POINTS.md | 1 - .../common/optimization/test_float_fusing.py | 5 --- tests/numpy/test_compile.py | 9 ----- tests/numpy/test_tracing.py | 33 ++++++++++++++++--- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index 00e1edfa6..f8316dc89 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -110,6 +110,15 @@ class NPTracer(BaseTracer): Callable: the tracing function that needs to be called to trace func """ tracing_func: Optional[Callable] + + # numpy.invert is not great in term of types it supports, so we've decided not to support it + # and to propose to the user to use numpy.bitwise_not + if func == numpy.invert: + raise RuntimeError( + f"NPTracer does not manage the following func: {func.__name__}. Please replace by " + f"calls to bitwise_xor with appropriate mask" + ) + if isinstance(func, numpy.ufunc): tracing_func = NPTracer.UFUNC_ROUTING.get(func, None) else: @@ -266,6 +275,9 @@ class NPTracer(BaseTracer): # numpy.isnat is not there since it is about timings # # numpy.divmod, numpy.modf and numpy.frexp are not there since output two values + # + # numpy.invert (as known as numpy.bitwise_not) is not here, because it has strange input type. + # We ask the user to replace bitwise_xor instead LIST_OF_SUPPORTED_UFUNC: List[numpy.ufunc] = [ numpy.absolute, numpy.arccos, @@ -301,7 +313,6 @@ class NPTracer(BaseTracer): numpy.greater_equal, numpy.heaviside, numpy.hypot, - numpy.invert, numpy.isfinite, numpy.isinf, numpy.isnan, diff --git a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md index afaaeb4d4..32b54d454 100644 --- a/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md +++ b/docs/user/tutorial/WORKING_WITH_FLOATING_POINTS.md @@ -48,7 +48,6 @@ List of supported unary functions: - expm1 - fabs - floor -- invert - isfinite - isinf - isnan diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index 0b715eef6..e9f13c3eb 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -224,10 +224,6 @@ def subtest_fuse_float_unary_operations_correctness(fun, tensor_shape): # Not too large values to avoid overflows input_list = [1, 2, 5, 11] super_fun_list = [mix_x_and_y_and_call_f, mix_x_and_y_intricately_and_call_f] - elif fun == numpy.invert: - # 0 is not in the domain of definition + expect integer inputs - input_list = [1, 2, 42, 44] - super_fun_list = [mix_x_and_y_into_integer_and_call_f] else: # Regular case input_list = [0, 2, 42, 44] @@ -307,7 +303,6 @@ LIST_OF_UFUNC_WHICH_HAVE_INTEGER_ONLY_SOURCES = { numpy.bitwise_or, numpy.bitwise_xor, numpy.gcd, - numpy.invert, numpy.lcm, numpy.ldexp, numpy.left_shift, diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 8bc025f86..51d96f64a 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -393,15 +393,6 @@ def test_unary_ufunc_operations(ufunc, default_compilation_configuration): ((0, 5), (0, 5)), default_compilation_configuration, ) - elif ufunc in [ - numpy.invert, - ]: - # Can't make it work, to have a fusable function - # TODO: fixme - pass - # subtest_compile_and_run_unary_ufunc_correctness( - # ufunc, mix_x_and_y_and_call_f_with_integer_inputs, ((0, 5), (0, 5)) - # ) elif ufunc in [ numpy.arccosh, numpy.log, diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 7c953a44c..3006f5d89 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -381,6 +381,35 @@ def test_tracing_astype( assert expected_output == evaluated_output +@pytest.mark.parametrize( + "inputs", + [ + pytest.param( + {"x": EncryptedScalar(Integer(32, is_signed=True))}, + ), + ], +) +@pytest.mark.parametrize( + "function_to_trace", + # We really need a lambda (because numpy functions are not playing + # nice with inspect.signature), but pylint is not happy + # with it + # pylint: disable=unnecessary-lambda + [lambda x: numpy.invert(x), lambda x: numpy.bitwise_not(x)], + # pylint: enable=unnecessary-lambda +) +def test_trace_numpy_fails_for_invert(inputs, function_to_trace): + """Check we catch calls to numpy.invert and tell user to change their code""" + + with pytest.raises(RuntimeError) as excinfo: + tracing.trace_numpy_function(function_to_trace, inputs) + + assert ( + "NPTracer does not manage the following func: invert. Please replace by calls to " + "bitwise_xor with appropriate mask" in str(excinfo.value) + ) + + @pytest.mark.parametrize( "inputs,expected_output_node", [ @@ -414,10 +443,6 @@ def test_tracing_astype( def test_trace_numpy_supported_unary_ufuncs(inputs, expected_output_node, function_to_trace_def): """Function to trace supported numpy ufuncs""" - # numpy.invert is expecting inputs which are integer only - if function_to_trace_def == numpy.invert and not isinstance(inputs["x"].dtype, Integer): - return - # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint and flake8 are not happy # with it From a15e31dda4b2a2151d51ec3b4f2ae3e36702bab7 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 19 Oct 2021 09:45:11 +0200 Subject: [PATCH 0442/1104] chore: move pytest-randomly to dev deps --- poetry.lock | 538 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 +- 2 files changed, 293 insertions(+), 247 deletions(-) diff --git a/poetry.lock b/poetry.lock index 931d6d1a4..bc9f82f32 100644 --- a/poetry.lock +++ b/poetry.lock @@ -32,7 +32,7 @@ tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"] [[package]] name = "astroid" -version = "2.8.0" +version = "2.8.3" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -41,13 +41,13 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = ">=1.11,<1.13" +wrapt = ">=1.11,<1.14" [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -55,7 +55,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -123,7 +123,7 @@ webencodings = "*" [[package]] name = "certifi" -version = "2021.5.30" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -131,7 +131,7 @@ python-versions = "*" [[package]] name = "cffi" -version = "1.14.6" +version = "1.15.0" description = "Foreign Function Interface for Python calling C code." category = "dev" optional = false @@ -150,7 +150,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.6" +version = "2.0.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -161,7 +161,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.1" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -235,7 +235,7 @@ six = "*" [[package]] name = "debugpy" -version = "1.5.0" +version = "1.5.1" description = "An implementation of the Debug Adapter Protocol for Python" category = "dev" optional = false @@ -259,7 +259,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "diff-cover" -version = "6.4.1" +version = "6.4.2" description = "Run coverage and linting reports on diffs" category = "dev" optional = false @@ -366,7 +366,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\" [[package]] name = "idna" -version = "3.2" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false @@ -384,7 +384,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "importlib-metadata" version = "4.8.1" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -412,7 +412,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -574,7 +574,7 @@ jinja2 = ">=2.4" [[package]] name = "jsonschema" -version = "4.0.1" +version = "4.1.0" description = "An implementation of JSON Schema validation for Python" category = "dev" optional = false @@ -1019,7 +1019,7 @@ python-versions = ">=3.7,<3.11" name = "packaging" version = "21.0" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1075,7 +1075,7 @@ python-versions = "*" [[package]] name = "pillow" -version = "8.3.2" +version = "8.4.0" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -1108,7 +1108,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1161,7 +1161,7 @@ python-versions = "*" name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1279,7 +1279,7 @@ python-versions = ">=3.6" name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1327,7 +1327,7 @@ pytest = ">=3.10" name = "pytest-randomly" version = "3.10.1" description = "Pytest plugin to randomly order tests and control random.seed." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1366,7 +1366,7 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "0.19.0" +version = "0.19.1" description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false @@ -1427,7 +1427,7 @@ python-versions = "*" [[package]] name = "pywin32" -version = "301" +version = "302" description = "Python for Window Extensions" category = "dev" optional = false @@ -1451,11 +1451,11 @@ python-versions = ">=3.6" [[package]] name = "pyyaml" -version = "5.4.1" +version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" [[package]] name = "pyzmq" @@ -1517,7 +1517,7 @@ md = ["cmarkgfm (>=0.5.0,<0.7.0)"] [[package]] name = "regex" -version = "2021.9.30" +version = "2021.10.8" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -1798,7 +1798,7 @@ test = ["pytest", "pathlib2"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" @@ -1933,17 +1933,17 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] [[package]] name = "wrapt" -version = "1.12.1" +version = "1.13.2" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -1954,7 +1954,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "7ab160d17a98f1b6dd4bf2083cc03875bf3abdde55b14334df3dbf7d1dd2233d" +content-hash = "0182aa76ab0bea8da33f79491b5d609d28160178703d078c754d3ca82d44de49" [metadata.files] alabaster = [ @@ -1979,8 +1979,8 @@ argon2-cffi = [ {file = "argon2_cffi-21.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:566ffb581bbd9db5562327aee71b2eda24a1c15b23a356740abe3c011bbe0dcb"}, ] astroid = [ - {file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"}, - {file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"}, + {file = "astroid-2.8.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"}, + {file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -2007,67 +2007,72 @@ bleach = [ {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, ] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] cffi = [ - {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"}, - {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"}, - {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"}, - {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"}, - {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"}, - {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"}, - {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"}, - {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"}, - {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"}, - {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"}, - {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"}, - {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"}, - {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"}, - {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"}, - {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"}, - {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"}, - {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"}, - {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"}, - {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"}, - {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"}, - {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"}, - {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"}, - {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"}, - {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"}, - {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"}, - {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"}, + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"}, - {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"}, + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] click-log = [ {file = "click-log-0.3.2.tar.gz", hash = "sha256:16fd1ca3fc6b16c98cea63acf1ab474ea8e676849dc669d86afafb0ed7003124"}, @@ -2139,27 +2144,27 @@ cycler = [ {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] debugpy = [ - {file = "debugpy-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:098753d30232d1e4264eee37e1ddd5d106dc5c4bc6d8d7f4dadad9e44736cd48"}, - {file = "debugpy-1.5.0-cp310-cp310-win32.whl", hash = "sha256:33e8a9b4949be8b4f5fcfff07e24bd63c565060659f1c79773c08d19eee012f2"}, - {file = "debugpy-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef71eb8eb276370f8e74ab3f8c7648bbdc9aabac814a5b2840c8dd38a7bc7251"}, - {file = "debugpy-1.5.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:dd0e8d5e099444c22b27511dafd48e8bdcd7051b811ddd0ab2062965fe36ac80"}, - {file = "debugpy-1.5.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:990228f15de4ccbc52c2accf41a63b3b8d0a01e3de9876e02e77e487c4b1ffab"}, - {file = "debugpy-1.5.0-cp36-cp36m-win32.whl", hash = "sha256:77b5233b23a248cd930bf03ecd684da065c6e7d2a57d137516b6fa1698a58317"}, - {file = "debugpy-1.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c3184666cfe1768bf110f8075bafea59d2afce3cc54f4c501f2371c7238bc69d"}, - {file = "debugpy-1.5.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1283e418f595262d11abc5fae6a3ac629c5fc3b44d3988511ea755414aab3062"}, - {file = "debugpy-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a03051ba4fdf6720ee83a42e9f803e3a0b69a48b00436b97d16aeda49d28a8bf"}, - {file = "debugpy-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:cdaf6baaf8176644e752aed321b3f810dcf8b0439709f7edd9ae542f849a639b"}, - {file = "debugpy-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:be7ca2baef5a634dfbd086d9c1d6b5e0783c6d0f6d0a004b43d36f625d4fc0a9"}, - {file = "debugpy-1.5.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:72093ea83226d5264b3697b948c07a3cfcc4953da14a78a50c4e623a2bb99ad8"}, - {file = "debugpy-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ce0794d50391c87813bb148548c34dc638fb4d58198d275334968f63c088aa69"}, - {file = "debugpy-1.5.0-cp38-cp38-win32.whl", hash = "sha256:de56775b3dbbfc02bc9fb0682da4a960e0a5bada699eac5e22e0723c4107ec9f"}, - {file = "debugpy-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:82c4fa1293981a28c435d196a3714e06df761daff0da3336234475ceff1b042c"}, - {file = "debugpy-1.5.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:8e7391a08a351adce6e5154ed35e4cf90c5f3c10dbf7c8f6a234faef300588d6"}, - {file = "debugpy-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dacdb0a3377063d638bd8736c80b7274ae341ce778fec0f883ef1cbb79538bf2"}, - {file = "debugpy-1.5.0-cp39-cp39-win32.whl", hash = "sha256:fda623aa1036b34d554a1225a09cae6bf02b06c0ad903a9f0b8ac3cb74eddc15"}, - {file = "debugpy-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:9f3bed64027bd80a8fe1f35491ec0ec2d2c85f1e63dac7c0311e400bfe58cf05"}, - {file = "debugpy-1.5.0-py2.py3-none-any.whl", hash = "sha256:f058c204341fd7ff800ee0edafc106ca0fb1c9857e8a8895a6e04cca3ddcb7bf"}, - {file = "debugpy-1.5.0.zip", hash = "sha256:86febd61fc351cee926060eef008e242b7259957d71d25eef82860d0cc59b4dc"}, + {file = "debugpy-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:70b422c63a833630c33e3f9cdbd9b6971f8c5afd452697e464339a21bbe862ba"}, + {file = "debugpy-1.5.1-cp310-cp310-win32.whl", hash = "sha256:3a457ad9c0059a21a6c7d563c1f18e924f5cf90278c722bd50ede6f56b77c7fe"}, + {file = "debugpy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:5d76a4fd028d8009c3faf1185b4b78ceb2273dd2499447664b03939e0368bb90"}, + {file = "debugpy-1.5.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:16db27b4b91991442f91d73604d32080b30de655aca9ba821b1972ea8171021b"}, + {file = "debugpy-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2b073ad5e8d8c488fbb6a116986858bab0c9c4558f28deb8832c7a5a27405bd6"}, + {file = "debugpy-1.5.1-cp36-cp36m-win32.whl", hash = "sha256:318f81f37341e4e054b4267d39896b73cddb3612ca13b39d7eea45af65165e1d"}, + {file = "debugpy-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b5b3157372e0e0a1297a8b6b5280bcf1d35a40f436c7973771c972726d1e32d5"}, + {file = "debugpy-1.5.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1ec3a086e14bba6c472632025b8fe5bdfbaef2afa1ebd5c6615ce6ed8d89bc67"}, + {file = "debugpy-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:26fbe53cca45a608679094791ce587b6e2798acd1d4777a8b303b07622e85182"}, + {file = "debugpy-1.5.1-cp37-cp37m-win32.whl", hash = "sha256:d876db8c312eeb02d85611e0f696abe66a2c1515e6405943609e725d5ff36f2a"}, + {file = "debugpy-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4404a62fb5332ea5c8c9132290eef50b3a0ba38cecacad5529e969a783bcbdd7"}, + {file = "debugpy-1.5.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f3a3dca9104aa14fd4210edcce6d9ce2b65bd9618c0b222135a40b9d6e2a9eeb"}, + {file = "debugpy-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2df2c373e85871086bd55271c929670cd4e1dba63e94a08d442db830646203b"}, + {file = "debugpy-1.5.1-cp38-cp38-win32.whl", hash = "sha256:82f5f9ce93af6861a0713f804e62ab390bb12a17f113153e47fea8bbb1dfbe36"}, + {file = "debugpy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:17a25ce9d7714f92fc97ef00cc06269d7c2b163094990ada30156ed31d9a5030"}, + {file = "debugpy-1.5.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:01e98c594b3e66d529e40edf314f849cd1a21f7a013298df58cd8e263bf8e184"}, + {file = "debugpy-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f73988422b17f071ad3c4383551ace1ba5ed810cbab5f9c362783d22d40a08dc"}, + {file = "debugpy-1.5.1-cp39-cp39-win32.whl", hash = "sha256:23df67fc56d59e386c342428a7953c2c06cc226d8525b11319153e96afb65b0c"}, + {file = "debugpy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2aa64f6d2ca7ded8a7e8a4e7cae3bc71866b09876b7b05cecad231779cb9156"}, + {file = "debugpy-1.5.1-py2.py3-none-any.whl", hash = "sha256:194f95dd3e84568b5489aab5689a3a2c044e8fdc06f1890b8b4f70b6b89f2778"}, + {file = "debugpy-1.5.1.zip", hash = "sha256:d2b09e91fbd1efa4f4fda121d49af89501beda50c18ed7499712c71a4bf3452e"}, ] decorator = [ {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, @@ -2170,8 +2175,8 @@ defusedxml = [ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, ] diff-cover = [ - {file = "diff_cover-6.4.1-py3-none-any.whl", hash = "sha256:0aa34983401d42f6f69a9c9be0fa5fe6f0cb9fa8ada4cca3a0145d6d8dd9bf74"}, - {file = "diff_cover-6.4.1.tar.gz", hash = "sha256:9d0296e25ca21b2235e70a0001acda498f52896b3453ea44d04cf53ceeb5ef72"}, + {file = "diff_cover-6.4.2-py3-none-any.whl", hash = "sha256:d2986e8b42556ed8c01792293b48720ec31451e91a7fa9cfc089d0d0e8bae84e"}, + {file = "diff_cover-6.4.2.tar.gz", hash = "sha256:d3cb6c89d625b5edbf90517c1bb2634b8ae0aef907e4f6c730cbe5ebf3fe3671"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, @@ -2205,8 +2210,8 @@ gitpython = [ {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, ] idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, @@ -2266,8 +2271,8 @@ jinja2-pluralize = [ {file = "jinja2_pluralize-0.3.0.tar.gz", hash = "sha256:df5c2d5017b9b54c0a66cb790cca9fc08945837c3dbfc323589203f1ffb73c1c"}, ] jsonschema = [ - {file = "jsonschema-4.0.1-py3-none-any.whl", hash = "sha256:9938802041347f2c62cad2aef59e9a0826cd34584f3609db950efacb4dbf6518"}, - {file = "jsonschema-4.0.1.tar.gz", hash = "sha256:48f4e74f8bec0c2f75e9fcfffa264e78342873e1b57e2cfeae54864cc5e9e4dd"}, + {file = "jsonschema-4.1.0-py3-none-any.whl", hash = "sha256:2b3cca28580511d44326f0e7fc582eab3cbe31aabd1a1c2cfa74a399796ffd84"}, + {file = "jsonschema-4.1.0.tar.gz", hash = "sha256:9dd7c33b4a96138dc37bb86b3610d3b12d30d96433d4d73435ca3025804154a8"}, ] jupyter = [ {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, @@ -2593,59 +2598,47 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pillow = [ - {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, - {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, - {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, - {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, - {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, - {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, - {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, - {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, - {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, - {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, - {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, - {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, - {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, - {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, - {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, - {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, - {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, - {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, - {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, - {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, - {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, - {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, - {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, - {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, - {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, - {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, + {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, + {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, + {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, + {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, + {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, + {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, + {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, + {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, + {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, + {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, + {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, + {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, + {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, + {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, + {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, + {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, + {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, + {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"}, + {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, ] pkginfo = [ {file = "pkginfo-1.7.1-py2.py3-none-any.whl", hash = "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779"}, @@ -2811,8 +2804,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-dotenv = [ - {file = "python-dotenv-0.19.0.tar.gz", hash = "sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"}, - {file = "python_dotenv-0.19.0-py2.py3-none-any.whl", hash = "sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1"}, + {file = "python-dotenv-0.19.1.tar.gz", hash = "sha256:14f8185cc8d494662683e6914addcb7e95374771e707601dfc70166946b4c4b8"}, + {file = "python_dotenv-0.19.1-py2.py3-none-any.whl", hash = "sha256:bbd3da593fc49c249397cbfbcc449cf36cb02e75afc8157fcc6a81df6fb7750a"}, ] python-gitlab = [ {file = "python-gitlab-2.10.1.tar.gz", hash = "sha256:7afa7d7c062fa62c173190452265a30feefb844428efc58ea5244f3b9fc0d40f"}, @@ -2827,16 +2820,16 @@ pytz = [ {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pywin32 = [ - {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"}, - {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"}, - {file = "pywin32-301-cp36-cp36m-win32.whl", hash = "sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0"}, - {file = "pywin32-301-cp36-cp36m-win_amd64.whl", hash = "sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78"}, - {file = "pywin32-301-cp37-cp37m-win32.whl", hash = "sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b"}, - {file = "pywin32-301-cp37-cp37m-win_amd64.whl", hash = "sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a"}, - {file = "pywin32-301-cp38-cp38-win32.whl", hash = "sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17"}, - {file = "pywin32-301-cp38-cp38-win_amd64.whl", hash = "sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96"}, - {file = "pywin32-301-cp39-cp39-win32.whl", hash = "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe"}, - {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"}, + {file = "pywin32-302-cp310-cp310-win32.whl", hash = "sha256:251b7a9367355ccd1a4cd69cd8dd24bd57b29ad83edb2957cfa30f7ed9941efa"}, + {file = "pywin32-302-cp310-cp310-win_amd64.whl", hash = "sha256:79cf7e6ddaaf1cd47a9e50cc74b5d770801a9db6594464137b1b86aa91edafcc"}, + {file = "pywin32-302-cp36-cp36m-win32.whl", hash = "sha256:fe21c2fb332d03dac29de070f191bdbf14095167f8f2165fdc57db59b1ecc006"}, + {file = "pywin32-302-cp36-cp36m-win_amd64.whl", hash = "sha256:d3761ab4e8c5c2dbc156e2c9ccf38dd51f936dc77e58deb940ffbc4b82a30528"}, + {file = "pywin32-302-cp37-cp37m-win32.whl", hash = "sha256:48dd4e348f1ee9538dd4440bf201ea8c110ea6d9f3a5010d79452e9fa80480d9"}, + {file = "pywin32-302-cp37-cp37m-win_amd64.whl", hash = "sha256:496df89f10c054c9285cc99f9d509e243f4e14ec8dfc6d78c9f0bf147a893ab1"}, + {file = "pywin32-302-cp38-cp38-win32.whl", hash = "sha256:e372e477d938a49266136bff78279ed14445e00718b6c75543334351bf535259"}, + {file = "pywin32-302-cp38-cp38-win_amd64.whl", hash = "sha256:543552e66936378bd2d673c5a0a3d9903dba0b0a87235ef0c584f058ceef5872"}, + {file = "pywin32-302-cp39-cp39-win32.whl", hash = "sha256:2393c1a40dc4497fd6161b76801b8acd727c5610167762b7c3e9fd058ef4a6ab"}, + {file = "pywin32-302-cp39-cp39-win_amd64.whl", hash = "sha256:af5aea18167a31efcacc9f98a2ca932c6b6a6d91ebe31f007509e293dea12580"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, @@ -2850,35 +2843,39 @@ pywinpty = [ {file = "pywinpty-1.1.4.tar.gz", hash = "sha256:cc700c9d5a9fcebf677ac93a4943ca9a24db6e2f11a5f0e7e8e226184c5036f7"}, ] pyyaml = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd"}, @@ -2942,47 +2939,53 @@ readme-renderer = [ {file = "readme_renderer-30.0.tar.gz", hash = "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"}, ] regex = [ - {file = "regex-2021.9.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66696c8336a1b5d1182464f3af3427cc760118f26d0b09a2ddc16a976a4d2637"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d87459ad3ab40cd8493774f8a454b2e490d8e729e7e402a0625867a983e4e02"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf6a1e023caf5e9a982f5377414e1aeac55198831b852835732cfd0a0ca5ff"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:255791523f80ea8e48e79af7120b4697ef3b74f6886995dcdb08c41f8e516be0"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e502f8d4e5ef714bcc2c94d499684890c94239526d61fdf1096547db91ca6aa6"}, - {file = "regex-2021.9.30-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4907fb0f9b9309a5bded72343e675a252c2589a41871874feace9a05a540241e"}, - {file = "regex-2021.9.30-cp310-cp310-win32.whl", hash = "sha256:3be40f720af170a6b20ddd2ad7904c58b13d2b56f6734ee5d09bbdeed2fa4816"}, - {file = "regex-2021.9.30-cp310-cp310-win_amd64.whl", hash = "sha256:c2b180ed30856dfa70cfe927b0fd38e6b68198a03039abdbeb1f2029758d87e7"}, - {file = "regex-2021.9.30-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6f2d2f93001801296fe3ca86515eb04915472b5380d4d8752f09f25f0b9b0ed"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fa7ba9ab2eba7284e0d7d94f61df7af86015b0398e123331362270d71fab0b9"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28040e89a04b60d579c69095c509a4f6a1a5379cd865258e3a186b7105de72c6"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f588209d3e4797882cd238195c175290dbc501973b10a581086b5c6bcd095ffb"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42952d325439ef223e4e9db7ee6d9087b5c68c5c15b1f9de68e990837682fc7b"}, - {file = "regex-2021.9.30-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cae4099031d80703954c39680323dabd87a69b21262303160776aa0e55970ca0"}, - {file = "regex-2021.9.30-cp36-cp36m-win32.whl", hash = "sha256:0de8ad66b08c3e673b61981b9e3626f8784d5564f8c3928e2ad408c0eb5ac38c"}, - {file = "regex-2021.9.30-cp36-cp36m-win_amd64.whl", hash = "sha256:b345ecde37c86dd7084c62954468a4a655fd2d24fd9b237949dd07a4d0dd6f4c"}, - {file = "regex-2021.9.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6f08187136f11e430638c2c66e1db091105d7c2e9902489f0dbc69b44c222b4"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b55442650f541d195a535ccec33078c78a9521973fb960923da7515e9ed78fa6"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87e9c489aa98f50f367fb26cc9c8908d668e9228d327644d7aa568d47e456f47"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2cb7d4909ed16ed35729d38af585673f1f0833e73dfdf0c18e5be0061107b99"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0861e7f6325e821d5c40514c551fd538b292f8cc3960086e73491b9c5d8291d"}, - {file = "regex-2021.9.30-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:81fdc90f999b2147fc62e303440c424c47e5573a9b615ed5d43a5b832efcca9e"}, - {file = "regex-2021.9.30-cp37-cp37m-win32.whl", hash = "sha256:8c1ad61fa024195136a6b7b89538030bd00df15f90ac177ca278df9b2386c96f"}, - {file = "regex-2021.9.30-cp37-cp37m-win_amd64.whl", hash = "sha256:e3770781353a4886b68ef10cec31c1f61e8e3a0be5f213c2bb15a86efd999bc4"}, - {file = "regex-2021.9.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c065d95a514a06b92a5026766d72ac91bfabf581adb5b29bc5c91d4b3ee9b83"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9925985be05d54b3d25fd6c1ea8e50ff1f7c2744c75bdc4d3b45c790afa2bcb3"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470f2c882f2672d8eeda8ab27992aec277c067d280b52541357e1acd7e606dae"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad0517df22a97f1da20d8f1c8cb71a5d1997fa383326b81f9cf22c9dadfbdf34"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e30838df7bfd20db6466fd309d9b580d32855f8e2c2e6d74cf9da27dcd9b63"}, - {file = "regex-2021.9.30-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b34d2335d6aedec7dcadd3f8283b9682fadad8b9b008da8788d2fce76125ebe"}, - {file = "regex-2021.9.30-cp38-cp38-win32.whl", hash = "sha256:e07049cece3462c626d650e8bf42ddbca3abf4aa08155002c28cb6d9a5a281e2"}, - {file = "regex-2021.9.30-cp38-cp38-win_amd64.whl", hash = "sha256:37868075eda024470bd0feab872c692ac4ee29db1e14baec103257bf6cc64346"}, - {file = "regex-2021.9.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d331f238a7accfbbe1c4cd1ba610d4c087b206353539331e32a8f05345c74aec"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6348a7ab2a502cbdd0b7fd0496d614007489adb7361956b38044d1d588e66e04"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b1cca6c23f19bee8dc40228d9c314d86d1e51996b86f924aca302fc8f8bf9"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f1125bc5172ab3a049bc6f4b9c0aae95a2a2001a77e6d6e4239fa3653e202b5"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:638e98d069b14113e8afba6a54d1ca123f712c0d105e67c1f9211b2a825ef926"}, - {file = "regex-2021.9.30-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a0b0db6b49da7fa37ca8eddf9f40a8dbc599bad43e64f452284f37b6c34d91c"}, - {file = "regex-2021.9.30-cp39-cp39-win32.whl", hash = "sha256:9910869c472e5a6728680ca357b5846546cbbd2ab3ad5bef986ef0bc438d0aa6"}, - {file = "regex-2021.9.30-cp39-cp39-win_amd64.whl", hash = "sha256:3b71213ec3bad9a5a02e049f2ec86b3d7c3e350129ae0f4e2f99c12b5da919ed"}, - {file = "regex-2021.9.30.tar.gz", hash = "sha256:81e125d9ba54c34579e4539a967e976a3c56150796674aec318b1b2f49251be7"}, + {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:094a905e87a4171508c2a0e10217795f83c636ccc05ddf86e7272c26e14056ae"}, + {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"}, + {file = "regex-2021.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b0f2f874c6a157c91708ac352470cb3bef8e8814f5325e3c5c7a0533064c6a24"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"}, + {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"}, + {file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"}, + {file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"}, + {file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"}, + {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"}, + {file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"}, + {file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"}, + {file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"}, + {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"}, + {file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"}, + {file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"}, + {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19b8f6d23b2dc93e8e1e7e288d3010e58fafed323474cf7f27ab9451635136d9"}, + {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"}, + {file = "regex-2021.10.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:951be934dc25d8779d92b530e922de44dda3c82a509cdb5d619f3a0b1491fafa"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"}, + {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"}, + {file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"}, + {file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"}, + {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6dcf53d35850ce938b4f044a43b33015ebde292840cef3af2c8eb4c860730fff"}, + {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"}, + {file = "regex-2021.10.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2ec1c106d3f754444abf63b31e5c4f9b5d272272a491fa4320475aba9e8157c"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"}, + {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"}, + {file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"}, + {file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"}, + {file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, @@ -3161,7 +3164,50 @@ win32-setctime = [ {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"}, ] wrapt = [ - {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, + {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"}, + {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"}, + {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"}, + {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"}, + {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"}, + {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"}, + {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"}, + {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"}, + {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"}, + {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"}, + {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"}, + {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"}, + {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"}, + {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"}, + {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"}, + {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"}, + {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"}, + {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"}, + {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"}, + {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"}, + {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"}, + {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, + {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, ] zipp = [ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, diff --git a/pyproject.toml b/pyproject.toml index 7676ba59e..c17bec5a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ numpy = "^1.21.2" pygraphviz = "^1.7" Pillow = "^8.3.2" loguru = "^0.5.3" -pytest-randomly = "^3.10.1" [tool.poetry.dev-dependencies] isort = "^5.9.3" @@ -44,6 +43,7 @@ semver = "^2.13.0" tomlkit = "^0.7.0" GitPython = "^3.1.24" pytest-xdist = "^2.4.0" +pytest-randomly = "^3.10.1" [build-system] requires = ["poetry-core>=1.0.0"] From ab151091ced3f9007824c0b9f8e39683c925cc8e Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 19 Oct 2021 11:05:40 +0200 Subject: [PATCH 0443/1104] chore(scripts): add a target to trigger a release - check the version coherence before creating the tag refs #308 --- .github/ISSUE_TEMPLATE/release.md | 3 ++- Makefile | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index bf5241ed8..ebadba434 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -20,7 +20,8 @@ VERSION=X.Y.Z-rc? make set_version Then: - [ ] For non RC releases: check the release milestone issues, cut out what can't be completed in time and change the milestones for these issues -- [ ] Checkout the commit for release, create a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) with the version name (careful for RC) `git tag -s -a -m "vX.Y.Z release" vX.Y.Z`, (or `vX.Y.Z-rc?`) push it to GitHub with `git push origin refs/tags/vX.Y.Z` (or `vX.Y.Z-rc?`) +- [ ] Checkout the commit for release +- [ ] Call `make release`, which creates a signed tag (requires GPG keys setup, see [here](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)) and pushes it - [ ] Wait for the release workflow to finish and check everything went well. To continue the release cycle: diff --git a/Makefile b/Makefile index d1af79bc2..d56d31a23 100644 --- a/Makefile +++ b/Makefile @@ -240,6 +240,15 @@ changelog: check_version_coherence poetry run python ./script/make_utils/changelog_helper.py > "CHANGELOG_$${PROJECT_VER}.md" .PHONY: changelog +release: check_version_coherence + @PROJECT_VER=($$(poetry version));\ + PROJECT_VER="$${PROJECT_VER[1]}";\ + TAG_NAME="v$${PROJECT_VER}";\ + git fetch --tags --force;\ + git tag -s -a -m "$${TAG_NAME} release" "$${TAG_NAME}";\ + git push origin "refs/tags/$${TAG_NAME}" +.PHONY: release + # Show the accepted types and optional scopes show_scope: @echo "Accepted types and optional scopes:" From 83ddf92bdd7eefeffc02f11ad24ccd5d4b763e66 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 19 Oct 2021 14:36:01 +0200 Subject: [PATCH 0444/1104] chore(ci): update workflow to push doc - check version and tag match - push doc as version if not pre-release - push doc as stable if version is the biggest one, clear cache in that case closes #454 --- .github/workflows/continuous-integration.yaml | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 8cdb49849..c86628a6d 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -502,8 +502,19 @@ jobs: python -m pip install poetry make setup_env - name: Set tag in env + # 'poetry version' cannot be piped properly so do it in 2 steps + # the project version does not have the leading v to be semver compatible run: | + PROJECT_VERSION=$(poetry version) + PROJECT_VERSION=$(echo "$PROJECT_VERSION" | cut -d ' ' -f 2) GIT_TAG=$(echo "${{ github.ref }}" | sed 's/refs\/tags\///g') + + if [[ "v${PROJECT_VERSION}" != "${GIT_TAG}" ]]; then + echo "Mismatch between tag and version: ${GIT_TAG}, v${PROJECT_VERSION}" + exit 1 + fi + + echo "PROJECT_VERSION=${PROJECT_VERSION}" >> "$GITHUB_ENV" echo "GIT_TAG=${GIT_TAG}" >> "$GITHUB_ENV" RELEASE_IMG_GIT_TAG="${RELEASE_IMAGE_BASE}:${GIT_TAG}" echo "RELEASE_IMG_GIT_TAG=${RELEASE_IMG_GIT_TAG}" >> "$GITHUB_ENV" @@ -600,15 +611,37 @@ jobs: docker image push --all-tags "${RELEASE_IMAGE_BASE}" - name: Push release documentation if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) }} - run: | - echo "Should push release documentation as ${GIT_TAG}" - echo "The dir to push would be: ${{ steps.download-docs.outputs.download-path }}" - echo "It contains:" - ls -la "${{ steps.download-docs.outputs.download-path }}" + uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 + with: + args: --delete --acl public-read + env: + AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SOURCE_DIR: ${{ steps.download-docs.outputs.download-path }} + DEST_DIR: 'concretefhe/${{ env.PROJECT_VERSION }}' - name: Push release documentation as stable if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} - run: | - echo "Should push release documentation as stable" + uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 + with: + args: --delete --acl public-read + env: + AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} + SOURCE_DIR: ${{ steps.download-docs.outputs.download-path }} + DEST_DIR: 'concretefhe/stable' + - name: Invalidate CloudFront Cache for stable + if: ${{ success() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} + uses: awact/cloudfront-action@8bcfabc7b4bbc0cb8e55e48527f0e3a6d681627c + env: + SOURCE_PATH: "/concretefhe/stable/*" + AWS_REGION: ${{ secrets.AWS_REGION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + DISTRIBUTION_ID: ${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }} - name: Create GitHub release if: ${{ success() && !cancelled() }} id: create-release From e707333f360c24b759b718aec456de8df8c04db7 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 19 Oct 2021 18:00:37 +0200 Subject: [PATCH 0445/1104] chore(ci): update release text generation closes #634 --- .github/workflows/continuous-integration.yaml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index c86628a6d..523caaa74 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -593,7 +593,7 @@ jobs: with: name: changelog path: ${{ env.ARTIFACTS_RAW_DIR }}/changelog/ - - name: Create ready to upload/packaged artifacts + - name: Create ready to upload/packaged artifacts and release body if: ${{ success() && !cancelled() }} env: RAW_DOCS_DIR: ${{ steps.download-docs.outputs.download-path }} @@ -604,7 +604,18 @@ jobs: tar -cvzf "${ARTIFACTS_PACKAGED_DIR}/html-docs.tar.gz" ./* popd cp "${RAW_CHANGELOG_DIR}"/* "${ARTIFACTS_PACKAGED_DIR}" - ls -a . + ls -a "${ARTIFACTS_PACKAGED_DIR}" + + RELEASE_BODY_FILE=RELEASE_BODY.md + echo "RELEASE_BODY_FILE=${RELEASE_BODY_FILE}" >> "$GITHUB_ENV" + + cp ./script/actions_utils/RELEASE_TEMPLATE.md "${RELEASE_BODY_FILE}" + echo "Docker Image: ${RELEASE_IMG_GIT_TAG}" >> "${RELEASE_BODY_FILE}" + if [[ "${IS_PRERELEASE}" == "false" ]]; then + echo "Documentation: https://docs.zama.ai/concretefhe/${PROJECT_VERSION}" >> "${RELEASE_BODY_FILE}" + fi + echo "" >> "${RELEASE_BODY_FILE}" + cat "${RAW_CHANGELOG_DIR}"/* >> "${RELEASE_BODY_FILE}" - name: Push release docker image if: ${{ success() && !cancelled() }} run: | @@ -647,9 +658,7 @@ jobs: id: create-release uses: softprops/action-gh-release@6034af24fba4e5a8e975aaa6056554efe4c794d0 with: - body: | - **Docker Image:** ${{ env.RELEASE_IMG_GIT_TAG }} - **Documentation:** https://docs.zama.ai/concrete/ + body_path: ${{ env.RELEASE_BODY_FILE }} prerelease: ${{ fromJSON(env.IS_PRERELEASE) }} files: | ${{ env.ARTIFACTS_PACKAGED_DIR }}/* From c16ac6ada8b42f6ab4892629fb3ba3f20a3d77f2 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 19 Oct 2021 18:51:58 +0200 Subject: [PATCH 0446/1104] chore(ci): add forgotten release template file --- script/actions_utils/RELEASE_TEMPLATE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 script/actions_utils/RELEASE_TEMPLATE.md diff --git a/script/actions_utils/RELEASE_TEMPLATE.md b/script/actions_utils/RELEASE_TEMPLATE.md new file mode 100644 index 000000000..b8c86727d --- /dev/null +++ b/script/actions_utils/RELEASE_TEMPLATE.md @@ -0,0 +1,5 @@ +## Summary + +_Please fill here with information about the main features in this release, or the main reason for having a delivery (e.g., fixing an annoying bug)_ + +## Links From 92de61f485b53b94a6d0a4cdf14b070fcefcf9bb Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 20 Oct 2021 10:01:05 +0200 Subject: [PATCH 0447/1104] chore: bump version to 0.2.0-rc3 --- concrete/version.py | 2 +- docs/conf.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/concrete/version.py b/concrete/version.py index 193a1ba82..bd13490b4 100644 --- a/concrete/version.py +++ b/concrete/version.py @@ -1,4 +1,4 @@ """Package version module.""" # Auto-generated by "make set_version" do not modify -__version__ = "0.2.0-rc2" +__version__ = "0.2.0-rc3" diff --git a/docs/conf.py b/docs/conf.py index 3500df8f9..8e6010a2c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ copyright = "2021, Zama" author = "Zama" # The full version, including alpha/beta/rc tags -release = "0.2.0-rc2" +release = "0.2.0-rc3" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index c17bec5a3..8fb2431f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.2.0-rc2" +version = "0.2.0-rc3" description = "Concrete Framework" authors = ["Zama "] packages = [ From be453394fb1d632f7cacf03a9074181cc126d1b2 Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 19 Oct 2021 12:17:17 +0300 Subject: [PATCH 0448/1104] fix(compilation): raise the appropriate error for intermediate signed integers --- concrete/common/data_types/dtypes_helpers.py | 12 +- concrete/common/mlir/utils.py | 84 ++++++++-- concrete/numpy/compile.py | 43 +++-- tests/common/debugging/test_printing.py | 8 +- tests/common/mlir/test_mlir_converter.py | 6 +- tests/numpy/test_compile.py | 157 +++++++++++++++++-- 6 files changed, 265 insertions(+), 45 deletions(-) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index bf48470ca..94f43ead0 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -83,16 +83,20 @@ def value_is_scalar(value_to_check: BaseValue) -> bool: return isinstance(value_to_check, TensorValue) and value_to_check.is_scalar -def value_is_integer(value_to_check: BaseValue) -> bool: - """Check that a value is of type Integer. +def value_is_unsigned_integer(value_to_check: BaseValue) -> bool: + """Check that a value is of type Integer and is unsigned. Args: value_to_check (BaseValue): The value to check Returns: - bool: True if the passed value_to_check is of type Integer + bool: True if the passed value_to_check is of type Integer and is unsigned """ - return isinstance(value_to_check.dtype, INTEGER_TYPES) + + return ( + isinstance(value_to_check.dtype, INTEGER_TYPES) + and not cast(Integer, value_to_check.dtype).is_signed + ) def value_is_encrypted_tensor_integer(value_to_check: BaseValue) -> bool: diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index d5e153419..172e133c4 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -7,17 +7,86 @@ from ..data_types.dtypes_helpers import ( value_is_clear_tensor_integer, value_is_encrypted_scalar_integer, value_is_encrypted_tensor_integer, - value_is_integer, value_is_scalar, + value_is_unsigned_integer, ) -from ..debugging.custom_assert import assert_true +from ..debugging.custom_assert import assert_not_reached, assert_true from ..operator_graph import OPGraph +from ..representation import intermediate from ..representation.intermediate import IntermediateNode, UnivariateFunction # TODO: should come from compiler, through an API, #402 ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB = 7 +def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) -> Optional[str]: + """Check if node is compatible with MLIR. + + Args: + node (IntermediateNode): node to check + is_output (bool): whether the node is an output node or not + + Returns: + Optional[str]: None if the node is compatible else reason for incompatibility + """ + + # pylint: disable=too-many-branches,too-many-return-statements + + inputs = node.inputs + outputs = node.outputs + + if isinstance(node, intermediate.Add): # constraints for addition + for inp in inputs: + if not value_is_scalar(inp): + return "only scalar addition is supported" + + elif isinstance(node, intermediate.Sub): # constraints for subtraction + for inp in inputs: + if not value_is_scalar(inp): + return "only scalar subtraction is supported" + + elif isinstance(node, intermediate.Mul): # constraints for multiplication + for inp in inputs: + if not value_is_scalar(inp): + return "only scalar multiplication is supported" + + elif isinstance(node, intermediate.Input): # constraints for inputs + assert_true(len(outputs) == 1) + if not value_is_unsigned_integer(outputs[0]): + return "only unsigned integer inputs are supported" + + elif isinstance(node, intermediate.Constant): # constraints for constants + assert_true(len(outputs) == 1) + if not value_is_unsigned_integer(outputs[0]): + return "only unsigned integer constants are supported" + + elif isinstance(node, intermediate.UnivariateFunction): # constraints for univariate functions + assert_true(len(inputs) == 1) + if not value_is_scalar(inputs[0]) or not value_is_unsigned_integer(inputs[0]): + return "only unsigned integer scalar lookup tables are supported" + + elif isinstance(node, intermediate.Dot): # constraints for dot product + assert_true(len(inputs) == 2) + if not value_is_unsigned_integer(inputs[0]) or not value_is_unsigned_integer(inputs[1]): + return "only unsigned integer dot product is supported" + + else: # pragma: no cover + assert_not_reached("Non IntermediateNode object in the OPGraph") + + if is_output: + for out in outputs: + if not value_is_scalar(out) or not value_is_unsigned_integer(out): + return "only scalar unsigned integer outputs are supported" + else: + for out in outputs: + if not value_is_unsigned_integer(out): + return "only unsigned integer intermediates are supported" + + # pylint: enable=too-many-branches,too-many-return-statements + + return None + + def check_graph_values_compatibility_with_mlir( op_graph: OPGraph, ) -> Optional[Dict[IntermediateNode, str]]: @@ -33,13 +102,10 @@ def check_graph_values_compatibility_with_mlir( offending_nodes = {} - for out_node in op_graph.output_nodes.values(): - for out in out_node.outputs: - if not value_is_scalar(out): - offending_nodes[out_node] = "non scalar outputs aren't supported" - - if value_is_integer(out) and cast(Integer, out.dtype).is_signed: - offending_nodes[out_node] = "signed integer outputs aren't supported" + for node in op_graph.graph.nodes: + is_output = node in op_graph.output_nodes.values() + if (reason := check_node_compatibility_with_mlir(node, is_output)) is not None: + offending_nodes[node] = reason return None if len(offending_nodes) == 0 else offending_nodes diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 5e754ddfd..59c6fc1a5 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -163,20 +163,6 @@ def _compile_numpy_function_into_op_graph_internal( # Add the initial graph as an artifact compilation_artifacts.add_operation_graph("final", op_graph) - # Make sure the graph can be lowered to MLIR - offending_nodes = check_graph_values_compatibility_with_mlir(op_graph) - if offending_nodes is not None: - raise RuntimeError( - "function you are trying to compile isn't supported for MLIR lowering\n\n" - + get_printable_graph(op_graph, show_data_types=True, highlighted_nodes=offending_nodes) - ) - - # Update bit_width for MLIR - update_bit_width_for_mlir(op_graph) - - # TODO: workaround extend LUT #359 - extend_direct_lookup_tables(op_graph) - return op_graph @@ -244,6 +230,33 @@ def compile_numpy_function_into_op_graph( raise +def prepare_op_graph_for_mlir(op_graph): + """Prepare OPGraph for MLIR lowering. + + This includes checking compatibility, changing bit-widths, and modifying lookup tables. + + Args: + op_graph (OPGraph): The operation graph to prepare + + Returns: + None + """ + + # Make sure the graph can be lowered to MLIR + offending_nodes = check_graph_values_compatibility_with_mlir(op_graph) + if offending_nodes is not None: + raise RuntimeError( + "function you are trying to compile isn't supported for MLIR lowering\n\n" + + get_printable_graph(op_graph, show_data_types=True, highlighted_nodes=offending_nodes) + ) + + # Update bit_width for MLIR + update_bit_width_for_mlir(op_graph) + + # TODO: workaround extend LUT #359 + extend_direct_lookup_tables(op_graph) + + def _compile_numpy_function_internal( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], @@ -281,6 +294,8 @@ def _compile_numpy_function_internal( compilation_artifacts, ) + prepare_op_graph_for_mlir(op_graph) + # Convert graph to an MLIR representation converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(op_graph) diff --git a/tests/common/debugging/test_printing.py b/tests/common/debugging/test_printing.py index e0bb62c49..c191d7052 100644 --- a/tests/common/debugging/test_printing.py +++ b/tests/common/debugging/test_printing.py @@ -45,9 +45,9 @@ return(%2) with_types == """ -%0 = x # EncryptedScalar> +%0 = x # EncryptedScalar> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ foo -%1 = Constant(42) # ClearScalar> +%1 = Constant(42) # ClearScalar> %2 = Add(%0, %1) # EncryptedScalar> return(%2) @@ -81,9 +81,9 @@ return(%2) with_types == """ -%0 = x # EncryptedScalar> +%0 = x # EncryptedScalar> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ foo -%1 = Constant(42) # ClearScalar> +%1 = Constant(42) # ClearScalar> %2 = Add(%0, %1) # EncryptedScalar> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ bar return(%2) diff --git a/tests/common/mlir/test_mlir_converter.py b/tests/common/mlir/test_mlir_converter.py index bb405a928..f5f772aba 100644 --- a/tests/common/mlir/test_mlir_converter.py +++ b/tests/common/mlir/test_mlir_converter.py @@ -13,7 +13,7 @@ from concrete.common.extensions.table import LookupTable from concrete.common.mlir import V0_OPSET_CONVERSION_FUNCTIONS from concrete.common.values import ClearScalar, EncryptedScalar from concrete.common.values.tensors import ClearTensor, EncryptedTensor -from concrete.numpy.compile import compile_numpy_function_into_op_graph +from concrete.numpy.compile import compile_numpy_function_into_op_graph, prepare_op_graph_for_mlir from concrete.numpy.np_mlir_converter import NPMLIRConverter @@ -225,6 +225,7 @@ def test_mlir_converter(func, args_dict, args_ranges, default_compilation_config inputset, default_compilation_configuration, ) + prepare_op_graph_for_mlir(result_graph) converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error @@ -270,6 +271,7 @@ def test_mlir_converter_dot_between_vectors( ), default_compilation_configuration, ) + prepare_op_graph_for_mlir(result_graph) converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) mlir_result = converter.convert(result_graph) # testing that this doesn't raise an error @@ -291,6 +293,7 @@ def test_mlir_converter_dot_vector_and_constant(default_compilation_configuratio [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], default_compilation_configuration, ) + prepare_op_graph_for_mlir(left_graph) left_converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) left_mlir = left_converter.convert(left_graph) @@ -300,6 +303,7 @@ def test_mlir_converter_dot_vector_and_constant(default_compilation_configuratio [(numpy.random.randint(0, 2 ** 3, size=(2,)),) for _ in range(10)], default_compilation_configuration, ) + prepare_op_graph_for_mlir(right_graph) right_converter = NPMLIRConverter(V0_OPSET_CONVERSION_FUNCTIONS) right_mlir = right_converter.convert(right_graph) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 51d96f64a..e26a82b13 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -13,6 +13,8 @@ from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor from concrete.numpy import tracing from concrete.numpy.compile import compile_numpy_function, compile_numpy_function_into_op_graph +# pylint: disable=too-many-lines + def no_fuse_unhandled(x, y): """No fuse unhandled""" @@ -746,7 +748,21 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long "%1 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ signed integer outputs aren't supported\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported\n" # noqa: E501 # pylint: disable=line-too-long + "return(%2)\n" + ), + ), + pytest.param( + lambda x: x + (-1), + {"x": EncryptedScalar(Integer(4, is_signed=False))}, + [(i,) for i in range(1, 2 ** 4)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = Constant(-1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long "return(%2)\n" ), ), @@ -760,7 +776,79 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ non scalar outputs aren't supported\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 # pylint: disable=line-too-long + "return(%2)\n" + ), + ), + pytest.param( + lambda x: x + 1, + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, + [(numpy.random.randint(0, 2 ** 3, size=(2, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 # pylint: disable=line-too-long + "return(%2)\n" + ), + ), + pytest.param( + lambda x: x * 1, + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, + [(numpy.random.randint(0, 2 ** 3, size=(2, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Mul(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar multiplication is supported\n" # noqa: E501 # pylint: disable=line-too-long + "return(%2)\n" + ), + ), + pytest.param( + lambda x: 127 - x, + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, + [(numpy.random.randint(0, 2 ** 3, size=(2, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = Constant(127) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Sub(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar subtraction is supported\n" # noqa: E501 # pylint: disable=line-too-long + "return(%2)\n" + ), + ), + pytest.param( + lambda x, y: numpy.dot(x, y), # pylint: disable=unnecessary-lambda + { + "x": EncryptedTensor(Integer(2, is_signed=True), shape=(1,)), + "y": EncryptedTensor(Integer(2, is_signed=True), shape=(1,)), + }, + [ + (numpy.array([-1]), numpy.array([-1])), + (numpy.array([-1]), numpy.array([0])), + (numpy.array([0]), numpy.array([-1])), + (numpy.array([0]), numpy.array([0])), + (numpy.array([1]), numpy.array([1])), + (numpy.array([1]), numpy.array([0])), + (numpy.array([0]), numpy.array([1])), + (numpy.array([0]), numpy.array([0])), + (numpy.array([-2]), numpy.array([-2])), + (numpy.array([-2]), numpy.array([1])), + ], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(1,)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = y # EncryptedTensor, shape=(1,)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = Dot(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer dot product is supported\n" # noqa: E501 # pylint: disable=line-too-long "return(%2)\n" ), ), @@ -769,15 +857,58 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura def test_fail_compile(function, parameters, inputset, match, default_compilation_configuration): """Test function compile_numpy_function_into_op_graph for a program with signed values""" - try: - compile_numpy_function( - function, - parameters, - inputset, - default_compilation_configuration, - ) - except RuntimeError as error: - assert str(error) == match + with pytest.raises(RuntimeError): + try: + compile_numpy_function( + function, + parameters, + inputset, + default_compilation_configuration, + ) + except RuntimeError as error: + assert str(error) == match + raise + + +def test_fail_with_intermediate_signed_values(default_compilation_configuration): + """Test function with failing compilation due to intermediate signed integers.""" + + def function(x, y): + z = numpy.abs(10 * numpy.negative(x)) + z = z.astype(numpy.int32) + y + return z + + with pytest.raises(RuntimeError): + try: + compile_numpy_function( + function, + { + "x": EncryptedScalar(Integer(2, is_signed=False)), + "y": EncryptedScalar(Integer(2, is_signed=False)), + }, + [(i, j) for i in range(2 ** 2) for j in range(2 ** 2)], + default_compilation_configuration, + show_mlir=True, + ) + except RuntimeError as error: + match = ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = y # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = Constant(10) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%2 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%3 = np.negative(%2) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%4 = Mul(%3, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%5 = np.absolute(%4) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%6 = astype(int32)(%5) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%7 = Add(%6, %0) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "return(%7)\n" + ) + assert str(error) == match + raise def test_small_inputset_no_fail(): @@ -818,9 +949,9 @@ def test_small_inputset_treat_warnings_as_errors(): # Remark that, when you do the dot of tensors of 4 values between 0 and 3, # you can get a maximal value of 4*3*3 = 36, ie something on 6 bits "%0 = x " - "# EncryptedTensor, shape=(4,)>" + "# EncryptedTensor, shape=(4,)>" "\n%1 = y " - "# EncryptedTensor, shape=(4,)>" + "# EncryptedTensor, shape=(4,)>" "\n%2 = Dot(%0, %1) " "# EncryptedScalar>" "\nreturn(%2)\n", From 806d6584e8e8ff6c90519bc8a53db4fba695a295 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 20 Oct 2021 14:56:48 +0200 Subject: [PATCH 0449/1104] feat: let us choose file to benchmark if needed, eg during development of new benchmarks, we can chose the files to benchmark with calls like: - poetry run python script/progress_tracker_utils/measure.py benchmarks -f benchmarks/x_matmul_y.py benchmarks/x_plus_y.py - poetry run python script/progress_tracker_utils/measure.py benchmarks -f benchmarks/x_matmul_y.py and the classical - poetry run python script/progress_tracker_utils/measure.py benchmarks is still usable --- script/progress_tracker_utils/measure.py | 39 +++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 01e91aa47..6f348f389 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -284,17 +284,17 @@ def perform_measurements(path, script, target_id, metrics, samples, result): del result["targets"][target_id]["measurements"] -def main(args): - """Measurement script for the progress tracker""" - +def get_scripts_to_benchmark(args): + """Get the list of files to benchmark""" base = pathlib.Path(args.base) - samples = args.samples - with open(".benchmarks/machine.json", "r", encoding="utf-8") as f: - machine = json.load(f) + if args.files_to_benchmark is None: + scripts = list(base.glob("*.py")) + else: + scripts = [pathlib.Path(f) for f in args.files_to_benchmark] - result = {"machine": machine, "metrics": {}, "targets": {}} - scripts = list(base.glob("*.py")) + print("Will benchmark following files:\n") + print(" - " + "\n - ".join(str(s) for s in scripts)) # Clear the previous temporary scripts directory shutil.rmtree(".benchmarks/scripts", ignore_errors=True) @@ -307,6 +307,21 @@ def main(args): # (e.g., we copy `benchmarks/common.py` to `.benchmarks/scripts/common.py` which allows # the modified `.benchmarks/scripts/x_plus_42.py` to access `common` module`) + return scripts + + +def main(args): + """Measurement script for the progress tracker""" + + samples = args.samples + + with open(".benchmarks/machine.json", "r", encoding="utf-8") as f: + machine = json.load(f) + + result = {"machine": machine, "metrics": {}, "targets": {}} + + scripts = get_scripts_to_benchmark(args) + # Process each script under the base directory for path in scripts: # Read the script line by line @@ -396,5 +411,13 @@ if __name__ == "__main__": parser.add_argument("base", type=str, help="directory which contains the benchmarks") parser.add_argument("--samples", type=int, default=30, help="number of samples to take") parser.add_argument("--keep", action="store_true", help="flag to keep measurement scripts") + parser.add_argument( + "--files_to_benchmark", + "-f", + nargs="+", + type=str, + default=None, + help="files to benchmark in base directory (with base directory as a prefix)", + ) main(parser.parse_args()) From 3a7274c90586d36628c1150a2aed3e4f12ac8609 Mon Sep 17 00:00:00 2001 From: jfrery Date: Tue, 19 Oct 2021 16:38:04 +0200 Subject: [PATCH 0450/1104] docs: alert user on PROJECT_SETUP.md that zama's specific environment is needed --- docs/dev/howto/PROJECT_SETUP.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/howto/PROJECT_SETUP.md b/docs/dev/howto/PROJECT_SETUP.md index 7bf215062..affd6afca 100644 --- a/docs/dev/howto/PROJECT_SETUP.md +++ b/docs/dev/howto/PROJECT_SETUP.md @@ -1,6 +1,10 @@ # Project Setup +```{note} +You will need Zama's specific environment with zamalang module to have the project fully functional. It is currently only delivered via the docker image (see the [docker](./DOCKER.md) guide). +``` + ## Installing Python v3.8 **concretefhe** is a `Python` library. So `Python` should be installed to develop **concretefhe**. `v3.8` is the only supported version. From 7f1222ed370dcee01b457cdc284eecb5c00029f7 Mon Sep 17 00:00:00 2001 From: Umut Date: Thu, 21 Oct 2021 13:25:56 +0300 Subject: [PATCH 0451/1104] feat(benchmarks): add a way to check benchmark scripts without running them --- Makefile | 7 ++++- script/progress_tracker_utils/measure.py | 38 +++++++++++++----------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index d56d31a23..7f3d06e7d 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,11 @@ check_finalize_nb: poetry run python ./script/nbmake_utils/notebook_finalize.py $(NOTEBOOKS_DIR) --check .PHONY: check_finalize_nb +check_benchmarks: + poetry run python script/progress_tracker_utils/extract_machine_info.py + poetry run python script/progress_tracker_utils/measure.py benchmarks --check +.PHONY: check_benchmarks + pylint: $(MAKE) --keep-going pylint_src pylint_tests pylint_benchmarks pylint_script .PHONY: pylint @@ -72,7 +77,7 @@ pcc: .PHONY: pcc PCC_DEPS := check_python_format check_finalize_nb python_linting mypy_ci pydocstyle shell_lint -PCC_DEPS += check_version_coherence check_supported_functions +PCC_DEPS += check_version_coherence check_supported_functions check_benchmarks pcc_internal: $(PCC_DEPS) .PHONY: pcc_internal diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 6f348f389..1d7b53e9c 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -293,8 +293,9 @@ def get_scripts_to_benchmark(args): else: scripts = [pathlib.Path(f) for f in args.files_to_benchmark] - print("Will benchmark following files:\n") - print(" - " + "\n - ".join(str(s) for s in scripts)) + if not args.check: + print("Will benchmark following files:\n") + print(" - " + "\n - ".join(str(s) for s in scripts)) # Clear the previous temporary scripts directory shutil.rmtree(".benchmarks/scripts", ignore_errors=True) @@ -344,7 +345,7 @@ def main(args): # Extract target name target_name = first_line.replace("# bench: Full Target:", "").strip() is_unit = False - else: + elif not args.check: print() print(path) print("-" * len(str(path))) @@ -382,33 +383,36 @@ def main(args): # Create another script to hold the modified version of the current script create_modified_script(name, lines, metrics) - # Create an entry in the result for the current target - result["targets"][target_id] = { - "name": target_name, - "measurements": {}, - "alerts": alerts, - "code": "\n".join(lines), - "isUnit": is_unit, - } + if not args.check: + # Create an entry in the result for the current target + result["targets"][target_id] = { + "name": target_name, + "measurements": {}, + "alerts": alerts, + "code": "\n".join(lines), + "isUnit": is_unit, + } - # Perform and save measurements - perform_measurements(path, name, target_id, metrics, samples, result) + # Perform and save measurements + perform_measurements(path, name, target_id, metrics, samples, result) - # Dump the latest results to the output file - with open(".benchmarks/findings.json", "w", encoding="utf-8") as f: - json.dump(result, f, indent=2, ensure_ascii=False) + # Dump the latest results to the output file + with open(".benchmarks/findings.json", "w", encoding="utf-8") as f: + json.dump(result, f, indent=2, ensure_ascii=False) # Delete the modified scripts if the user doesn't care if not args.keep: shutil.rmtree(".benchmarks/scripts", ignore_errors=True) - print() + if not args.check: + print() if __name__ == "__main__": parser = argparse.ArgumentParser(description="Measurement script for the progress tracker") parser.add_argument("base", type=str, help="directory which contains the benchmarks") + parser.add_argument("--check", action="store_true", help="flag to enable just checking mode") parser.add_argument("--samples", type=int, default=30, help="number of samples to take") parser.add_argument("--keep", action="store_true", help="flag to keep measurement scripts") parser.add_argument( From f1d28c0fad4615128a8579fcb41601539cb03717 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 21 Oct 2021 15:27:15 +0200 Subject: [PATCH 0452/1104] chore(benchmarks): manage comma and point signs --- script/progress_tracker_utils/measure.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/progress_tracker_utils/measure.py b/script/progress_tracker_utils/measure.py index 1d7b53e9c..62bdb69b3 100644 --- a/script/progress_tracker_utils/measure.py +++ b/script/progress_tracker_utils/measure.py @@ -23,6 +23,8 @@ def name_to_id(name): name = name.replace(" ", "-") name = name.replace("(", "") name = name.replace(")", "") + name = name.replace(",", "") + name = name.replace(".", "-") return urllib.parse.quote_plus(name.lower()) From 6edba1e10ccddd7cea927b7f9b29856036d5be13 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 20 Oct 2021 16:31:11 +0200 Subject: [PATCH 0453/1104] chore(benchmarks): add benchmark scripts for more features refs #700 --- benchmarks/c_concatenate_x.py | 49 ++++++++++++++++++++++++++++++++ benchmarks/c_matmul_x.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_concatenate_c.py | 49 ++++++++++++++++++++++++++++++++ benchmarks/x_concatenate_y.py | 53 +++++++++++++++++++++++++++++++++++ benchmarks/x_matmul_c.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_matmul_y.py | 53 +++++++++++++++++++++++++++++++++++ benchmarks/x_negative.py | 48 +++++++++++++++++++++++++++++++ benchmarks/x_plus_42.py | 6 ++-- benchmarks/x_plus_42_10b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_11b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_12b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_13b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_14b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_15b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_16b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_32b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_8b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_plus_42_9b.py | 50 +++++++++++++++++++++++++++++++++ benchmarks/x_reshape.py | 48 +++++++++++++++++++++++++++++++ benchmarks/x_tranpose.py | 48 +++++++++++++++++++++++++++++++ 20 files changed, 951 insertions(+), 3 deletions(-) create mode 100644 benchmarks/c_concatenate_x.py create mode 100644 benchmarks/c_matmul_x.py create mode 100644 benchmarks/x_concatenate_c.py create mode 100644 benchmarks/x_concatenate_y.py create mode 100644 benchmarks/x_matmul_c.py create mode 100644 benchmarks/x_matmul_y.py create mode 100644 benchmarks/x_negative.py create mode 100644 benchmarks/x_plus_42_10b.py create mode 100644 benchmarks/x_plus_42_11b.py create mode 100644 benchmarks/x_plus_42_12b.py create mode 100644 benchmarks/x_plus_42_13b.py create mode 100644 benchmarks/x_plus_42_14b.py create mode 100644 benchmarks/x_plus_42_15b.py create mode 100644 benchmarks/x_plus_42_16b.py create mode 100644 benchmarks/x_plus_42_32b.py create mode 100644 benchmarks/x_plus_42_8b.py create mode 100644 benchmarks/x_plus_42_9b.py create mode 100644 benchmarks/x_reshape.py create mode 100644 benchmarks/x_tranpose.py diff --git a/benchmarks/c_concatenate_x.py b/benchmarks/c_concatenate_x.py new file mode 100644 index 000000000..939f52062 --- /dev/null +++ b/benchmarks/c_concatenate_x.py @@ -0,0 +1,49 @@ +# bench: Unit Target: np.concatenate((c, x)) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return np.concatenate((c, x)) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(4, 5)) + c = np.arange(20).reshape((4, 5)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(4, 5)),) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(4, 5)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/c_matmul_x.py b/benchmarks/c_matmul_x.py new file mode 100644 index 000000000..cb04999eb --- /dev/null +++ b/benchmarks/c_matmul_x.py @@ -0,0 +1,50 @@ +# bench: Unit Target: np.matmul(c, x) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + c = np.arange(20, 30).reshape((5, 2)) + + def function_to_compile(x): + return np.matmul(c, x) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 4)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(2, 4))) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(2, 4)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_concatenate_c.py b/benchmarks/x_concatenate_c.py new file mode 100644 index 000000000..8392c154d --- /dev/null +++ b/benchmarks/x_concatenate_c.py @@ -0,0 +1,49 @@ +# bench: Unit Target: np.concatenate((x, c)) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return np.concatenate((x, c)) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(4, 5)) + c = np.arange(20).reshape((4, 5)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(4, 5)),) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(4, 5)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_concatenate_y.py b/benchmarks/x_concatenate_y.py new file mode 100644 index 000000000..e0b0e599b --- /dev/null +++ b/benchmarks/x_concatenate_y.py @@ -0,0 +1,53 @@ +# bench: Unit Target: np.concatenate((x, y)) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return np.concatenate((x, y)) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(4, 5)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(4, 5)) + + inputset = [ + (np.random.randint(0, 2 ** 3, size=(4, 5)), np.random.randint(0, 2 ** 3, size=(4, 5))) + for _ in range(128) + ] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(4, 5)) + sample_y = np.random.randint(0, 2 ** 3, size=(4, 5)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_matmul_c.py b/benchmarks/x_matmul_c.py new file mode 100644 index 000000000..a63c4797a --- /dev/null +++ b/benchmarks/x_matmul_c.py @@ -0,0 +1,50 @@ +# bench: Unit Target: np.matmul(x, c) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + c = np.arange(20).reshape((4, 5)) + + def function_to_compile(x): + return np.matmul(x, c) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 4)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(2, 4))) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(2, 4)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_matmul_y.py b/benchmarks/x_matmul_y.py new file mode 100644 index 000000000..13ab3e485 --- /dev/null +++ b/benchmarks/x_matmul_y.py @@ -0,0 +1,53 @@ +# bench: Unit Target: np.matmul(x, y) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x, y): + return np.matmul(x, y) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 4)) + y = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(4, 5)) + + inputset = [ + (np.random.randint(0, 2 ** 3, size=(2, 4)), np.random.randint(0, 2 ** 3, size=(4, 5))) + for _ in range(128) + ] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(2, 4)) + sample_y = np.random.randint(0, 2 ** 3, size=(4, 5)) + + inputs.append([sample_x, sample_y]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x, "y": y}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_negative.py b/benchmarks/x_negative.py new file mode 100644 index 000000000..0dff55aba --- /dev/null +++ b/benchmarks/x_negative.py @@ -0,0 +1,48 @@ +# bench: Unit Target: np.negative(x) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return np.negative(x) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(10, 6)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(10, 6)),) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(10, 6)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index 9b28ddff3..1f9075999 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -11,13 +11,13 @@ def main(): def function_to_compile(x): return x + 42 - x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) + x = hnp.EncryptedScalar(hnp.UnsignedInteger(10)) # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - [(i,) for i in range(2 ** 3)], + [(i,) for i in range(2 ** 10)], compilation_configuration=BENCHMARK_CONFIGURATION, ) # bench: Measure: End @@ -25,7 +25,7 @@ def main(): inputs = [] labels = [] for _ in range(4): - sample_x = random.randint(0, 2 ** 3 - 1) + sample_x = random.randint(0, 2 ** 10 - 1) inputs.append([sample_x]) labels.append(function_to_compile(*inputs[-1])) diff --git a/benchmarks/x_plus_42_10b.py b/benchmarks/x_plus_42_10b.py new file mode 100644 index 000000000..a04045ee0 --- /dev/null +++ b/benchmarks/x_plus_42_10b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (10b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 10 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** max_precision - 42)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_11b.py b/benchmarks/x_plus_42_11b.py new file mode 100644 index 000000000..dab6dc583 --- /dev/null +++ b/benchmarks/x_plus_42_11b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (11b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 11 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_12b.py b/benchmarks/x_plus_42_12b.py new file mode 100644 index 000000000..8bd6005d6 --- /dev/null +++ b/benchmarks/x_plus_42_12b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (12b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 12 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_13b.py b/benchmarks/x_plus_42_13b.py new file mode 100644 index 000000000..d8d18d1ea --- /dev/null +++ b/benchmarks/x_plus_42_13b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (13b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 13 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_14b.py b/benchmarks/x_plus_42_14b.py new file mode 100644 index 000000000..2e51a4acf --- /dev/null +++ b/benchmarks/x_plus_42_14b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (14b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 14 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_15b.py b/benchmarks/x_plus_42_15b.py new file mode 100644 index 000000000..1411de3e9 --- /dev/null +++ b/benchmarks/x_plus_42_15b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (15b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 15 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_16b.py b/benchmarks/x_plus_42_16b.py new file mode 100644 index 000000000..5f6ce8ee3 --- /dev/null +++ b/benchmarks/x_plus_42_16b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (16b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 16 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_32b.py b/benchmarks/x_plus_42_32b.py new file mode 100644 index 000000000..45b9cba5b --- /dev/null +++ b/benchmarks/x_plus_42_32b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (32b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 32 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(random.randint(0, 2 ** max_precision - 1 - 42),) for _ in range(128)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_8b.py b/benchmarks/x_plus_42_8b.py new file mode 100644 index 000000000..2c0aeb5fb --- /dev/null +++ b/benchmarks/x_plus_42_8b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (8b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 8 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** max_precision - 42)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_plus_42_9b.py b/benchmarks/x_plus_42_9b.py new file mode 100644 index 000000000..b68cfc963 --- /dev/null +++ b/benchmarks/x_plus_42_9b.py @@ -0,0 +1,50 @@ +# bench: Unit Target: x + 42 (9b) + +import random + +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + + max_precision = 9 + + def function_to_compile(x): + return x + 42 + + x = hnp.EncryptedScalar(hnp.UnsignedInteger(max_precision)) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + [(i,) for i in range(2 ** max_precision - 42)], + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + inputs = [] + labels = [] + for _ in range(4): + sample_x = random.randint(0, 2 ** max_precision - 1 - 42) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_reshape.py b/benchmarks/x_reshape.py new file mode 100644 index 000000000..36461e713 --- /dev/null +++ b/benchmarks/x_reshape.py @@ -0,0 +1,48 @@ +# bench: Unit Target: np.reshape(x, some_shape) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return np.reshape(x, (15, 4)) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(10, 6)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(10, 6)),) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(10, 6)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() diff --git a/benchmarks/x_tranpose.py b/benchmarks/x_tranpose.py new file mode 100644 index 000000000..2965c9deb --- /dev/null +++ b/benchmarks/x_tranpose.py @@ -0,0 +1,48 @@ +# bench: Unit Target: np.transpose(x) + +import numpy as np +from common import BENCHMARK_CONFIGURATION + +import concrete.numpy as hnp + + +def main(): + def function_to_compile(x): + return np.transpose(x) + + x = hnp.EncryptedTensor(hnp.UnsignedInteger(3), shape=(2, 4)) + + inputset = [(np.random.randint(0, 2 ** 3, size=(2, 4)),) for _ in range(128)] + + inputs = [] + labels = [] + for _ in range(4): + sample_x = np.random.randint(0, 2 ** 3, size=(2, 4)) + + inputs.append([sample_x]) + labels.append(function_to_compile(*inputs[-1])) + + # bench: Measure: Compilation Time (ms) + engine = hnp.compile_numpy_function( + function_to_compile, + {"x": x}, + inputset, + compilation_configuration=BENCHMARK_CONFIGURATION, + ) + # bench: Measure: End + + correct = 0 + for input_i, label_i in zip(inputs, labels): + # bench: Measure: Evaluation Time (ms) + result_i = engine.run(*input_i) + # bench: Measure: End + + if result_i == label_i: + correct += 1 + + # bench: Measure: Accuracy (%) = (correct / len(inputs)) * 100 + # bench: Alert: Accuracy (%) != 100 + + +if __name__ == "__main__": + main() From 946f0c07c7e63c3ad238717008ebc5470cbb0423 Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Thu, 14 Oct 2021 09:15:25 +0200 Subject: [PATCH 0454/1104] chore: update docs theme to v0.6.2 --- docs/_static/css/zama.css | 20 ++++++++++++++ docs/_templates/layout.html | 31 +++++++++------------- docs/_templates/versioning.html | 8 ++++++ docs/conf.py | 3 +++ poetry.lock | 47 +++++++++++---------------------- pyproject.toml | 1 + 6 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 docs/_templates/versioning.html diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index c6bc81012..b9dbbc6f2 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -53,6 +53,7 @@ body { font-family: var(--primary-font); + color: black; } /* Change code blocks font size slightly (normally 12px)*/ @@ -124,6 +125,25 @@ input[type=color], input[type=date], input[type=datetime-local], input[type=date background-color: #696969; } +/* increase padding for top menu + version bar */ +.wy-menu-vertical { + padding-bottom: 60px; +} + +/* fix version box font */ +.rst-versions { + font-family: var(--primary-font); +} +/* fix version fa-caret-down empty space on click */ +.rst-versions.shift-up { + overflow-y: unset; +} +/* current version color */ +.rst-versions .rst-current-version { + color: var(--primary-color) +} + + .wy-nav-content a { color: var(--link-color); text-decoration: underline; diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index f6769705e..cb864e715 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -16,22 +16,25 @@ - + + {#- Do not conflict with RTD insertion of analytics script #} + {%- if not READTHEDOCS %} + {%- if theme_analytics_id %} - {#- Do not conflict with RTD insertion of analytics script #} - {%- if not READTHEDOCS %} - {%- if theme_analytics_id %} +
+ + {{ project }} + {{ version_name }} + + +
diff --git a/docs/conf.py b/docs/conf.py index 8e6010a2c..52190907b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,6 +71,7 @@ html_favicon = "_static/favicon.ico" html_theme_options = { "logo_only": False, "analytics_id": "G-XRM93J9QBW", + "display_version": True, } pygments_style = "zenburn" html_last_updated_fmt = None # '%b %d, %Y' @@ -78,6 +79,8 @@ html_show_copyright = True html_show_sphinx = False html_context = { "show_docs_home_link": True, # Show/hide docs link in top menu + "versions_url": "/concretefhe/versions.html", + "version_name": release, } # Add any paths that contain custom static files (such as style sheets) here, diff --git a/poetry.lock b/poetry.lock index bc9f82f32..d755bc035 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1234,6 +1234,17 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "pygments-style-tomorrow" +version = "1.0.0.1" +description = "Pygments version of the tomorrow theme, Based on mozmorris/tomorrow-pygments." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pygments = ">=1.5" + [[package]] name = "pygraphviz" version = "1.7" @@ -1954,7 +1965,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "0182aa76ab0bea8da33f79491b5d609d28160178703d078c754d3ca82d44de49" +content-hash = "779f0b068ef721247370db40ff067c6207e70a922fa6e66958f30ede6a86c502" [metadata.files] alabaster = [ @@ -2382,22 +2393,12 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2406,21 +2407,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2430,9 +2424,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2745,6 +2736,10 @@ pygments = [ {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] +pygments-style-tomorrow = [ + {file = "pygments-style-tomorrow-1.0.0.1.tar.gz", hash = "sha256:4132c31f11738f6bed52d43a3f187aa3b889a118f9a29b40d1f6afb5bb1037be"}, + {file = "pygments_style_tomorrow-1.0.0.1-py3-none-any.whl", hash = "sha256:f5060ce35d598bf063a092db499782160b94d3d79f0fec96d527f20b5cd12743"}, +] pygraphviz = [ {file = "pygraphviz-1.7.zip", hash = "sha256:a7bec6609f37cf1e64898c59f075afd659106cf9356c5f387cecaa2e0cdb2304"}, ] @@ -2883,32 +2878,24 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, - {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"}, - {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"}, {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, - {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"}, - {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"}, {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, - {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"}, - {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"}, {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, - {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"}, - {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"}, {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, @@ -2916,8 +2903,6 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, - {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"}, - {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"}, {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, diff --git a/pyproject.toml b/pyproject.toml index 8fb2431f4..d54ee0458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ tomlkit = "^0.7.0" GitPython = "^3.1.24" pytest-xdist = "^2.4.0" pytest-randomly = "^3.10.1" +pygments-style-tomorrow = "^1.0.0" [build-system] requires = ["poetry-core>=1.0.0"] From 0b864afb76e7353db4d6c73c0bc845213b5fb4af Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Mon, 11 Oct 2021 11:04:55 +0200 Subject: [PATCH 0455/1104] docs: add versions.html template --- docs/versions.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/versions.html diff --git a/docs/versions.html b/docs/versions.html new file mode 100644 index 000000000..7f3b15535 --- /dev/null +++ b/docs/versions.html @@ -0,0 +1,23 @@ + + + + + + + + +
+
+

ConcreteFHE Documentation

+
+

Pick a version

+ +
+
+
+ + From bc90ed37ff7621a34d9bbe1c57ff0bd322dd3805 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Thu, 21 Oct 2021 19:27:07 +0200 Subject: [PATCH 0456/1104] chore(debugging): show problems in a clearer way with highlighted_nodes --- concrete/common/mlir/utils.py | 19 +++++++++++-------- tests/numpy/test_compile.py | 13 +++++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 172e133c4..d8694709f 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -10,6 +10,7 @@ from ..data_types.dtypes_helpers import ( value_is_scalar, value_is_unsigned_integer, ) +from ..debugging import get_printable_graph from ..debugging.custom_assert import assert_not_reached, assert_true from ..operator_graph import OPGraph from ..representation import intermediate @@ -134,7 +135,7 @@ def update_bit_width_for_mlir(op_graph: OPGraph): op_graph: graph to update bit_width for """ max_bit_width = 0 - offending_list = [] + offending_nodes = {} for node in op_graph.graph.nodes: for value_out in node.outputs: if value_is_clear_scalar_integer(value_out) or value_is_clear_tensor_integer(value_out): @@ -152,18 +153,20 @@ def update_bit_width_for_mlir(op_graph: OPGraph): # Check that current_node_out_bit_width is supported by the compiler if current_node_out_bit_width > ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB: - offending_list.append((node, current_node_out_bit_width)) + offending_nodes[ + node + ] = f"{current_node_out_bit_width} bits is not supported for the time being" - _set_all_bit_width(op_graph, max_bit_width) - - # Check that the max_bit_width is supported by the compiler - if len(offending_list) != 0: + if len(offending_nodes) != 0: raise RuntimeError( f"max_bit_width of some nodes is too high for the current version of " - f"the compiler (maximum must be {ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB} " - f"which is not compatible with {offending_list})" + f"the compiler (maximum must be {ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB}) " + f"which is not compatible with:\n" + + get_printable_graph(op_graph, show_data_types=True, highlighted_nodes=offending_nodes) ) + _set_all_bit_width(op_graph, max_bit_width) + def extend_direct_lookup_tables(op_graph: OPGraph): """Extend direct lookup tables to the maximum length the input bit width can support. diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index e26a82b13..bf583f13e 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -1049,12 +1049,17 @@ def test_compile_too_high_bitwidth(default_compilation_configuration): default_compilation_configuration, ) + # pylint: disable=line-too-long assert ( - "max_bit_width of some nodes is too high for the current version of the " - "compiler (maximum must be 7 which is not compatible with" in str(excinfo.value) + str(excinfo.value) + == "max_bit_width of some nodes is too high for the current version of the compiler (maximum must be 7) which is not compatible with:\n" # noqa: E501 + "%0 = x # EncryptedScalar>\n" # noqa: E501 + "%1 = y # EncryptedScalar>\n" # noqa: E501 + "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 bits is not supported for the time being\n" # noqa: E501 + "return(%2)\n" ) - - assert str(excinfo.value).endswith(", 8)])") + # pylint: enable=line-too-long # Just ok input_ranges = [(0, 99), (0, 28)] From 76d6f1e1f1e0e3867d61fdb3c9953ef02d40b241 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 22 Oct 2021 16:50:17 +0200 Subject: [PATCH 0457/1104] chore: bump version to 0.2.0-rc4 --- concrete/version.py | 2 +- docs/conf.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/concrete/version.py b/concrete/version.py index bd13490b4..cdfea93ab 100644 --- a/concrete/version.py +++ b/concrete/version.py @@ -1,4 +1,4 @@ """Package version module.""" # Auto-generated by "make set_version" do not modify -__version__ = "0.2.0-rc3" +__version__ = "0.2.0-rc4" diff --git a/docs/conf.py b/docs/conf.py index 52190907b..07c520371 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ copyright = "2021, Zama" author = "Zama" # The full version, including alpha/beta/rc tags -release = "0.2.0-rc3" +release = "0.2.0-rc4" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d54ee0458..8f8ae9139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "concretefhe" -version = "0.2.0-rc3" +version = "0.2.0-rc4" description = "Concrete Framework" authors = ["Zama "] packages = [ From fbfaeb2b1708bba6edf72d24d1bd1b0a24da3bbb Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 19 Oct 2021 16:14:46 +0200 Subject: [PATCH 0458/1104] feat: add table deduplication to NPMLIRConverter closes #560 closes #561 --- concrete/common/mlir/converters.py | 13 +++- concrete/numpy/np_mlir_converter.py | 61 ++++++++++++++- tests/numpy/test_np_mlir_converter.py | 106 ++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 tests/numpy/test_np_mlir_converter.py diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index 915b9dc4c..fe691c1d6 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -181,7 +181,18 @@ def apply_lut(node, preds, ir_to_mlir_node, ctx, additional_conversion_info): x_node = preds[0] x = ir_to_mlir_node[x_node] - table = additional_conversion_info["tables"][node] + tables = additional_conversion_info["tables"][node] + + # TODO: #559 adapt the code to support multi TLUs + # This cannot be reached today as compilation fails if the intermediate values are not all + # scalars + if len(tables) > 1: # pragma: no cover + raise RuntimeError( + "MLIR conversion currently does not support multiple test vectors for LUT" + ) + + table = tables[0][0] + out_dtype = cast(Integer, node.outputs[0].dtype) # Create table dense_elem = DenseElementsAttr.get(np.array(table, dtype=np.uint64), context=ctx) diff --git a/concrete/numpy/np_mlir_converter.py b/concrete/numpy/np_mlir_converter.py index 320a76a4c..8e2848fc7 100644 --- a/concrete/numpy/np_mlir_converter.py +++ b/concrete/numpy/np_mlir_converter.py @@ -1,14 +1,71 @@ """Numpy-specific MLIR converter.""" -from typing import Any, Dict +import math +from collections import defaultdict +from itertools import product +from typing import Any, DefaultDict, Dict, List, Tuple import numpy +from ..common.debugging import assert_true from ..common.mlir.mlir_converter import MLIRConverter from ..common.operator_graph import OPGraph from ..common.representation.intermediate import UnivariateFunction +class HashableNPArray: + """Class to easily manipulate numpy arrays for hashing. + + Note that the hash behavior won't work if the array is modified after being hashed, as it will + have been hashed to a certain value and the new array content will be hashed to a different one. + """ + + array: numpy.ndarray + + def __init__(self, array: numpy.ndarray) -> None: + self.array = array + + def __hash__(self) -> int: + return hash(self.array.tobytes()) + + def __eq__(self, other: object) -> bool: + return isinstance(other, HashableNPArray) and numpy.array_equal(self.array, other.array) + + +def generate_deduplicated_tables( + node: UnivariateFunction, +) -> Tuple[Tuple[numpy.ndarray, List[Tuple[int, ...]]], ...]: + """Deduplicate the tables for the different cells of a tensor if needed. + + Args: + node (UnivariateFunction): the node for which to deduplicate the table + + Returns: + Tuple[Tuple[numpy.ndarray, List[Tuple[int, ...]]], ...]: A tuple containing tuples whose + first element is a table and the second element is a list of tuples indicating which + cells in the tensor will use that table. + """ + # This is the tensor containing the tables for each cell of the tensor for node + node_complete_table = numpy.concatenate( + tuple(numpy.expand_dims(array, -1) for array in node.get_table()), axis=-1 + ) + + all_cells_idx = product(*tuple(range(max_val) for max_val in node_complete_table.shape[:-1])) + tables_to_cell_idx: DefaultDict[HashableNPArray, List[Tuple[int, ...]]] = defaultdict(list) + idx: Tuple[int, ...] + all_idx_set = set() + for idx in all_cells_idx: + hashable_array = HashableNPArray(node_complete_table[idx]) + tables_to_cell_idx[hashable_array].append(idx) + all_idx_set.add(idx) + + assert_true(len(all_idx_set) == math.prod(node_complete_table.shape[:-1])) + + return tuple( + (hashable_array.array, indices) for hashable_array, indices in tables_to_cell_idx.items() + ) + + class NPMLIRConverter(MLIRConverter): """Numpy-specific MLIR converter.""" @@ -28,7 +85,7 @@ class NPMLIRConverter(MLIRConverter): # Disable numpy warnings during conversion to avoid issues during TLU generation with numpy.errstate(all="ignore"): additional_conversion_info["tables"] = { - node: node.get_table() + node: generate_deduplicated_tables(node) for node in op_graph.graph.nodes() if isinstance(node, UnivariateFunction) } diff --git a/tests/numpy/test_np_mlir_converter.py b/tests/numpy/test_np_mlir_converter.py new file mode 100644 index 000000000..9edeed8e8 --- /dev/null +++ b/tests/numpy/test_np_mlir_converter.py @@ -0,0 +1,106 @@ +"""Test file for numpy mlir converter""" + +import math + +import numpy +import pytest + +import concrete.numpy as hnp +from concrete.common.representation.intermediate import UnivariateFunction +from concrete.numpy.np_mlir_converter import generate_deduplicated_tables + + +def multi_tlu_func(x, cst): + """Multi TLU function""" + y = x + cst + return y.astype(numpy.int32) + + +RESNET_BIGGEST_SHAPE = (64, 112, 112) +RESNET_BIGGEST_SIZE = math.prod(RESNET_BIGGEST_SHAPE) + + +@pytest.mark.parametrize( + "function,expected_number_of_tables", + [ + ( + lambda x: multi_tlu_func(x, numpy.zeros(RESNET_BIGGEST_SHAPE, dtype=numpy.float64)), + 1, + ), + ( + lambda x: multi_tlu_func( + x, + numpy.arange(RESNET_BIGGEST_SIZE, dtype=numpy.float64).reshape( + RESNET_BIGGEST_SHAPE + ), + ), + RESNET_BIGGEST_SIZE, + ), + ], +) +def test_generate_deduplicated_tables( + function, expected_number_of_tables, default_compilation_configuration +): + """Test function for generate_deduplicated_tables""" + op_graph = hnp.compile_numpy_function_into_op_graph( + function, + {"x": hnp.EncryptedTensor(hnp.Integer(7, False), RESNET_BIGGEST_SHAPE)}, + ((i * numpy.ones(RESNET_BIGGEST_SHAPE, dtype=numpy.int32),) for i in range(128)), + default_compilation_configuration, + ) + + univariate_function_nodes = [ + node for node in op_graph.graph.nodes() if isinstance(node, UnivariateFunction) + ] + + assert len(univariate_function_nodes) == 1 + + tlu_node = univariate_function_nodes[0] + + deduplication_result = generate_deduplicated_tables(tlu_node) + + assert len(deduplication_result) == expected_number_of_tables + + +def test_deduplicated_tables_correctness(default_compilation_configuration): + """Check the deduplicated tables are the expected ones""" + + tensor_shape = (2, 2) + + op_graph = hnp.compile_numpy_function_into_op_graph( + lambda x: multi_tlu_func(x, numpy.arange(4, dtype=numpy.float64).reshape(tensor_shape)), + {"x": hnp.EncryptedTensor(hnp.Integer(2, False), tensor_shape)}, + ((i * numpy.ones(tensor_shape, dtype=numpy.int32),) for i in range(4)), + default_compilation_configuration, + ) + + univariate_function_nodes = [ + node for node in op_graph.graph.nodes() if isinstance(node, UnivariateFunction) + ] + + assert len(univariate_function_nodes) == 1 + + tlu_node = univariate_function_nodes[0] + + deduplication_result = generate_deduplicated_tables(tlu_node) + + expected_result = tuple( + ( + numpy.arange(i, 4 + i, dtype=numpy.int32), + [ + numpy.unravel_index(i, tensor_shape), + ], + ) + for i in range(4) + ) + + assert len(deduplication_result) == len(expected_result) + for computed_array, computed_idx in deduplication_result: + for expected_array, expected_idx in expected_result: + if numpy.array_equal(computed_array, expected_array) and computed_idx == expected_idx: + break + else: + raise AssertionError( + f"Could not find {(computed_array, computed_idx)} " + f"in expected_result: {expected_result}" + ) From fc908fb3f5ed4ef4f81590f14a5591bdfd72f1e3 Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 22 Oct 2021 18:24:47 +0200 Subject: [PATCH 0459/1104] chore(benchmarks): fix the mistake in the change of the benchmark script --- benchmarks/x_plus_42.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmarks/x_plus_42.py b/benchmarks/x_plus_42.py index 1f9075999..9b28ddff3 100644 --- a/benchmarks/x_plus_42.py +++ b/benchmarks/x_plus_42.py @@ -11,13 +11,13 @@ def main(): def function_to_compile(x): return x + 42 - x = hnp.EncryptedScalar(hnp.UnsignedInteger(10)) + x = hnp.EncryptedScalar(hnp.UnsignedInteger(3)) # bench: Measure: Compilation Time (ms) engine = hnp.compile_numpy_function( function_to_compile, {"x": x}, - [(i,) for i in range(2 ** 10)], + [(i,) for i in range(2 ** 3)], compilation_configuration=BENCHMARK_CONFIGURATION, ) # bench: Measure: End @@ -25,7 +25,7 @@ def main(): inputs = [] labels = [] for _ in range(4): - sample_x = random.randint(0, 2 ** 10 - 1) + sample_x = random.randint(0, 2 ** 3 - 1) inputs.append([sample_x]) labels.append(function_to_compile(*inputs[-1])) From 9a41a57be04bc75945f6d7cfbdbc42fc4877628c Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 22 Oct 2021 18:04:38 +0200 Subject: [PATCH 0460/1104] fix: report the correct coverage status on exit closes #719 --- tests/conftest.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index aca5efd9b..c30fce90b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,7 +34,7 @@ def pytest_addoption(parser): ) -def pytest_sessionfinish(session: pytest.Session, exitstatus): +def pytest_sessionfinish(session: pytest.Session, exitstatus): # pylint: disable=unused-argument """Pytest callback when testing ends.""" # Hacked together from the source code, they don't have an option to export to file and it's too # much work to get a PR in for such a little thing @@ -47,9 +47,17 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus): if global_coverage_file is not None: cov_plugin = session.config.pluginmanager.getplugin("_cov") coverage_txt = cov_plugin.cov_report.getvalue() + coverage_status = 0 + if ( + cov_plugin.options.cov_fail_under is not None + and cov_plugin.options.cov_fail_under > 0 + ): + failed = cov_plugin.cov_total < cov_plugin.options.cov_fail_under + # If failed is False coverage_status is 0, if True it's 1 + coverage_status = int(failed) global_coverage_file_path = Path(global_coverage_file).resolve() with open(global_coverage_file_path, "w", encoding="utf-8") as f: - json.dump({"exit_code": exitstatus, "content": coverage_txt}, f) + json.dump({"exit_code": coverage_status, "content": coverage_txt}, f) def _is_equivalent_to_binary_commutative(lhs: IntermediateNode, rhs: object) -> bool: From 5aa87168f61a13a15de1e79ef3a6be2227e2cb1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Oct 2021 03:05:56 +0000 Subject: [PATCH 0461/1104] chore(deps): bump marocchino/sticky-pull-request-comment Bumps [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) from 2.1.1 to 2.2.0. - [Release notes](https://github.com/marocchino/sticky-pull-request-comment/releases) - [Commits](https://github.com/marocchino/sticky-pull-request-comment/compare/82e7a0d3c51217201b3fedc4ddde6632e969a477...39c5b5dc7717447d0cba270cd115037d32d28443) --- updated-dependencies: - dependency-name: marocchino/sticky-pull-request-comment dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/continuous-integration.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 523caaa74..b13e50e50 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -295,7 +295,7 @@ jobs: name: coverage path: coverage.html - name: Comment with coverage - uses: marocchino/sticky-pull-request-comment@82e7a0d3c51217201b3fedc4ddde6632e969a477 + uses: marocchino/sticky-pull-request-comment@39c5b5dc7717447d0cba270cd115037d32d28443 if: ${{ steps.coverage.outcome != 'skipped' && !cancelled() }} continue-on-error: true with: From afb342aec37e3c7e4c5a74952a108637440242ad Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 25 Oct 2021 09:42:04 +0200 Subject: [PATCH 0462/1104] chore: update Makefile targets to use && instead of ; - lets target fail properly closes #725 --- Makefile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 7f3d06e7d..8d592bf42 100644 --- a/Makefile +++ b/Makefile @@ -231,7 +231,7 @@ set_version: @if [[ "$$VERSION" == "" ]]; then \ echo "VERSION env variable is empty. Please set to desired version."; \ exit 1; \ - fi; + fi && \ poetry run python ./script/make_utils/version_utils.py set-version --version "$${VERSION}" .PHONY: set_version @@ -240,17 +240,17 @@ check_version_coherence: .PHONY: check_version_coherence changelog: check_version_coherence - PROJECT_VER=($$(poetry version));\ - PROJECT_VER="$${PROJECT_VER[1]}";\ + PROJECT_VER=($$(poetry version)) && \ + PROJECT_VER="$${PROJECT_VER[1]}" && \ poetry run python ./script/make_utils/changelog_helper.py > "CHANGELOG_$${PROJECT_VER}.md" .PHONY: changelog release: check_version_coherence - @PROJECT_VER=($$(poetry version));\ - PROJECT_VER="$${PROJECT_VER[1]}";\ - TAG_NAME="v$${PROJECT_VER}";\ - git fetch --tags --force;\ - git tag -s -a -m "$${TAG_NAME} release" "$${TAG_NAME}";\ + @PROJECT_VER=($$(poetry version)) && \ + PROJECT_VER="$${PROJECT_VER[1]}" && \ + TAG_NAME="v$${PROJECT_VER}" && \ + git fetch --tags --force && \ + git tag -s -a -m "$${TAG_NAME} release" "$${TAG_NAME}" && \ git push origin "refs/tags/$${TAG_NAME}" .PHONY: release @@ -268,7 +268,7 @@ show_type:show_scope # exclude notebooks (sometimes matches in svg text), match the notes in this directory todo: @NOTES_ARGS=$$(poetry run python ./script/make_utils/get_pylintrc_notes.py \ - --pylintrc-path pylintrc);\ + --pylintrc-path pylintrc) && \ grep -rInH --exclude-dir='.[^.]*' --exclude=pylintrc --exclude='*.ipynb' "$${NOTES_ARGS}" . .PHONY: todo From 65af96253b2d5862c77db8b0aded1c0dce5a5545 Mon Sep 17 00:00:00 2001 From: Umut Date: Wed, 20 Oct 2021 17:23:16 +0300 Subject: [PATCH 0463/1104] feat(tracing): implement tracing of constant indexing --- concrete/common/debugging/drawing.py | 2 + concrete/common/debugging/printing.py | 11 +- concrete/common/helpers/__init__.py | 3 + concrete/common/helpers/indexing_helpers.py | 277 ++++++++ concrete/common/mlir/utils.py | 5 + .../common/representation/intermediate.py | 62 +- concrete/common/tracing/base_tracer.py | 5 + concrete/numpy/np_indexing_helpers.py | 59 ++ concrete/numpy/tracing.py | 9 + .../representation/test_intermediate.py | 53 +- tests/conftest.py | 11 + tests/numpy/test_compile.py | 14 + tests/numpy/test_compile_constant_indexing.py | 598 ++++++++++++++++++ 13 files changed, 1105 insertions(+), 4 deletions(-) create mode 100644 concrete/common/helpers/__init__.py create mode 100644 concrete/common/helpers/indexing_helpers.py create mode 100644 concrete/numpy/np_indexing_helpers.py create mode 100644 tests/numpy/test_compile_constant_indexing.py diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index f9f1e02f3..c348e5c5d 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -16,6 +16,7 @@ from ..representation.intermediate import ( Add, Constant, Dot, + IndexConstant, Input, Mul, Sub, @@ -29,6 +30,7 @@ IR_NODE_COLOR_MAPPING = { Sub: "yellow", Mul: "green", UnivariateFunction: "orange", + IndexConstant: "black", Dot: "purple", "UnivariateFunction": "orange", "TLU": "grey", diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index 0077ce794..cfca8696b 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -6,7 +6,13 @@ import networkx as nx from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph -from ..representation.intermediate import Constant, Input, IntermediateNode, UnivariateFunction +from ..representation.intermediate import ( + Constant, + IndexConstant, + Input, + IntermediateNode, + UnivariateFunction, +) def output_data_type_to_string(node): @@ -124,6 +130,9 @@ def get_printable_graph( what_to_print += prefix_to_add_to_what_to_print what_to_print += ", ".join(["%" + x[1] for x in list_of_arg_name]) what_to_print += suffix_to_add_to_what_to_print + what_to_print += ( + f"{node.label().replace('value', '')}" if isinstance(node, IndexConstant) else "" + ) what_to_print += ")" # This code doesn't work with more than a single output diff --git a/concrete/common/helpers/__init__.py b/concrete/common/helpers/__init__.py new file mode 100644 index 000000000..908c72ca0 --- /dev/null +++ b/concrete/common/helpers/__init__.py @@ -0,0 +1,3 @@ +"""Helpers for all kinds of tasks.""" + +from . import indexing_helpers diff --git a/concrete/common/helpers/indexing_helpers.py b/concrete/common/helpers/indexing_helpers.py new file mode 100644 index 000000000..80b77fede --- /dev/null +++ b/concrete/common/helpers/indexing_helpers.py @@ -0,0 +1,277 @@ +"""Helpers for indexing functionality.""" + +from typing import Tuple, Union + + +def format_indexing_element(indexing_element: Union[int, slice]) -> str: + """Format an indexing element. + + This is required mainly for slices. The reason is that string representation of slices + are very long and verbose. To give an example, `x[:, 2:]` will have the following index + `[slice(None, None, None), slice(2, None, None)]` if printed naively. With this helper, + it will be formatted as `[:, 2:]`. + + Args: + indexing_element (Union[int, slice]): indexing element to be formatted + + Returns: + str: formatted element + """ + + result = "" + if isinstance(indexing_element, slice): + if indexing_element.start is not None: + result += str(indexing_element.start) + result += ":" + if indexing_element.stop is not None: + result += str(indexing_element.stop) + if indexing_element.step is not None: + result += ":" + result += str(indexing_element.step) + else: + result += str(indexing_element) + return result.replace("\n", " ") + + +def validate_index( + index: Union[int, slice, Tuple[Union[int, slice], ...]], +) -> Tuple[Union[int, slice], ...]: + """Make sure index is valid and convert it to the tuple form. + + For example in `x[2]`, `index` is passed as `2`. + To make it easier to work with, this function converts index to `(2,)`. + + Args: + index (Union[int, slice, Tuple[Union[int, slice], ...]]): index to validate, improve + and return + + Returns: + Tuple[Union[int, slice], ...]: validated and improved index + """ + + if not isinstance(index, tuple): + index = (index,) + + for indexing_element in index: + valid = isinstance(indexing_element, (int, slice)) + + if isinstance(indexing_element, slice): + if ( + not (indexing_element.start is None or isinstance(indexing_element.start, int)) + or not (indexing_element.stop is None or isinstance(indexing_element.stop, int)) + or not (indexing_element.step is None or isinstance(indexing_element.step, int)) + ): + valid = False + + if not valid: + raise TypeError( + f"Only integers and integer slices can be used for indexing " + f"but you tried to use {format_indexing_element(indexing_element)} for indexing" + ) + + return index + + +def determine_output_shape( + input_shape: Tuple[int, ...], + index: Tuple[Union[int, slice], ...], +) -> Tuple[int, ...]: + """Determine the output shape from the input shape and the index. + + e.g., for `input_shape=(3, 2)` and `index=(:, 0)`, returns `(3,)` + for `input_shape=(4, 3, 2)` and `index=(2:,)`, returns `(2, 3, 2)` + + Args: + input_shape (Tuple[int, ...]): shape of the input tensor that is indexed + index (Tuple[Union[int, slice], ...]): desired and validated index + + Returns: + Tuple[int, ...]: shape of the result of indexing + """ + + indexing_elements = [format_indexing_element(indexing_element) for indexing_element in index] + index_str = f"[{', '.join(indexing_elements)}]" + + if len(index) > len(input_shape): + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"as the index has more elements than the number of dimensions of the tensor" + ) + + # indexing (3, 4, 5) with [1] is the same as indexing it with [1, :, :] + # indexing (3, 4, 5) with [1, 2] is the same as indexing it with [1, 2, :] + + # so let's replicate that behavior to make the rest of the code generic + index += (slice(None, None, None),) * (len(input_shape) - len(index)) + + output_shape = [] + for dimension, (indexing_element, dimension_size) in enumerate(zip(index, input_shape)): + if isinstance(indexing_element, int): # indexing removes the dimension + indexing_element = ( + indexing_element if indexing_element >= 0 else indexing_element + dimension_size + ) + if not 0 <= indexing_element < dimension_size: + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"because index is out of range for dimension {dimension}" + ) + elif isinstance(indexing_element, slice): # indexing possibly shrinks the dimension + output_shape.append( + determine_new_dimension_size( + indexing_element, + dimension_size, + dimension, + input_shape, + index_str, + ) + ) + + return tuple(output_shape) + + +def sanitize_start_index( + start: int, + dimension_size: int, + # the rest is used for detailed exception message + dimension: int, + input_shape: Tuple[int, ...], + index_str: str, +) -> int: + """Sanitize and check start index of a slice. + + Args: + start (int): start index being sanitized + dimension_size (int): size of the dimension the slice is applied to + dimension (int): index of the dimension being sliced (for better messages) + input_shape (Tuple[int, ...]): shape of the whole input (for better messages) + index_str (str): string representation of the whole index (for better messages) + + Returns: + int: sanitized start index + """ + + start = start if start >= 0 else start + dimension_size + if not 0 <= start < dimension_size: + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"because start index is out of range for dimension {dimension}" + ) + return start + + +def sanitize_stop_index( + stop: int, + dimension_size: int, + # the rest is used for detailed exception message + dimension: int, + input_shape: Tuple[int, ...], + index_str: str, +) -> int: + """Sanitize and check stop index of a slice. + + Args: + stop (int): stop index being sanitized + dimension_size (int): size of the dimension the slice is applied to + dimension (int): index of the dimension being sliced (for better messages) + input_shape (Tuple[int, ...]): shape of the whole input (for better messages) + index_str (str): string representation of the whole index (for better messages) + + Returns: + int: sanitized stop index + """ + + stop = stop if stop >= 0 else stop + dimension_size + if not 0 <= stop <= dimension_size: + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"because stop index is out of range for dimension {dimension}" + ) + return stop + + +def determine_new_dimension_size( + slice_: slice, + dimension_size: int, + # the rest is used for detailed exception message + dimension: int, + input_shape: Tuple[int, ...], + index_str: str, +) -> int: + """Determine the new size of a dimension from the old size and the slice applied to it. + + e.g., for `slice_=1:4` and `dimension_size=5`, returns `3` + for `slice_=::-1` and `dimension_size=5`, returns `5` + + You may want to check this page to learn more about how this function works + https://numpy.org/doc/stable/reference/arrays.indexing.html#basic-slicing-and-indexing + + Args: + slice_ (slice): slice being applied to the dimension + dimension_size (int): size of the dimension the slice is applied to + dimension (int): index of the dimension being sliced (for better messages) + input_shape (Tuple[int, ...]): shape of the whole input (for better messages) + index_str (str): string representation of the whole index (for better messages) + + Returns: + int: new size of the dimension + """ + + step = slice_.step if slice_.step is not None else 1 + + if step > 0: + start = slice_.start if slice_.start is not None else 0 + stop = slice_.stop if slice_.stop is not None else dimension_size + + start = sanitize_start_index(start, dimension_size, dimension, input_shape, index_str) + stop = sanitize_stop_index(stop, dimension_size, dimension, input_shape, index_str) + + if start >= stop: + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"because start index is not less than stop index for dimension {dimension}" + ) + + size_before_stepping = stop - start + elif step < 0: + start = slice_.start if slice_.start is not None else dimension_size - 1 + stop = slice_.stop + + start = sanitize_start_index(start, dimension_size, dimension, input_shape, index_str) + + if stop is None: + # this is a weird case but it works as expected + # the issue is that it's impossible to slice whole vector reversed + # with a stop value different than none + + # if `x.shape == (6,)` the only one that works is `x[::-1].shape == (6,)` + # here is what doesn't work (and this is expected it's just weird) + # + # ... + # `x[:-2:-1].shape == (1,)` + # `x[:-1:-1].shape == (0,)` (note that this is a hard error for us) + # `x[:0:-1].shape == (5,)` + # `x[:1:-1].shape == (4,)` + # ... + + size_before_stepping = start + 1 + else: + stop = sanitize_stop_index(stop, dimension_size, dimension, input_shape, index_str) + + if stop >= start: + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"because step is negative and " + f"stop index is not less than start index for dimension {dimension}" + ) + + size_before_stepping = start - stop + else: + raise ValueError( + f"Tensor of shape {input_shape} cannot be indexed with {index_str} " + f"because step is zero for dimension {dimension}" + ) + + quotient = size_before_stepping // abs(step) + remainder = size_before_stepping % abs(step) + + return quotient + (remainder != 0) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index d8694709f..4dcc60d86 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -71,6 +71,11 @@ def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) if not value_is_unsigned_integer(inputs[0]) or not value_is_unsigned_integer(inputs[1]): return "only unsigned integer dot product is supported" + elif isinstance(node, intermediate.IndexConstant): # constraints for constant indexing + assert_true(len(outputs) == 1) + if not value_is_unsigned_integer(outputs[0]): + return "only unsigned integer tensor constant indexing is supported" + else: # pragma: no cover assert_not_reached("Non IntermediateNode object in the OPGraph") diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index c96029b64..884801b47 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from collections import deque from copy import deepcopy -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union from loguru import logger @@ -14,7 +14,15 @@ from ..data_types.dtypes_helpers import ( ) from ..data_types.integers import Integer from ..debugging.custom_assert import assert_true -from ..values import BaseValue, ClearScalar, EncryptedScalar, TensorValue +from ..helpers import indexing_helpers +from ..values import ( + BaseValue, + ClearScalar, + ClearTensor, + EncryptedScalar, + EncryptedTensor, + TensorValue, +) IR_MIX_VALUES_FUNC_ARG_NAME = "mix_values_func" @@ -197,6 +205,56 @@ class Constant(IntermediateNode): return str(self.constant_data) +class IndexConstant(IntermediateNode): + """Node representing a constant indexing in the program. + + What we mean by constant indexing is that the index part of the operation is a constant. + Here are some examples: `x[2]`, `x[0, 1]`, `y[:, 0]`, `y[3:, :5]` + + The opposite is to have dynamic indexing, which this node does not support. + Some examples of dynamic indexing are: `x[y]`, `x[y, z]`, `x[:, y]` + """ + + _n_in: int = 1 + + index: Tuple[Union[int, slice], ...] + + def __init__( + self, + input_: BaseValue, + index: Union[int, slice, Tuple[Union[int, slice], ...]], + ) -> None: + super().__init__((input_,)) + + if not isinstance(self.inputs[0], TensorValue) or self.inputs[0].is_scalar: + raise TypeError(f"Only tensors can be indexed but you tried to index {self.inputs[0]}") + + self.index = indexing_helpers.validate_index(index) + + output_dtype = self.inputs[0].dtype + output_shape = indexing_helpers.determine_output_shape(self.inputs[0].shape, self.index) + + self.outputs = [ + EncryptedTensor(output_dtype, output_shape) + if self.inputs[0].is_encrypted + else ClearTensor(output_dtype, output_shape) + ] + + def evaluate(self, inputs: Dict[int, Any]) -> Any: + return inputs[0][self.index] + + def label(self) -> str: + """Label of the node to show during drawings. + + It can be used for some other places after `"value"` below is replaced by `""`. + This note will no longer be necessary after #707 is addressed. + + """ + elements = [indexing_helpers.format_indexing_element(element) for element in self.index] + index = ", ".join(elements) + return f"value[{index}]" + + def flood_replace_none_values(table: list): """Use a flooding algorithm to replace None values. diff --git a/concrete/common/tracing/base_tracer.py b/concrete/common/tracing/base_tracer.py index 4a6f450e4..68bacbabe 100644 --- a/concrete/common/tracing/base_tracer.py +++ b/concrete/common/tracing/base_tracer.py @@ -7,6 +7,7 @@ from ..debugging.custom_assert import assert_true from ..representation.intermediate import ( IR_MIX_VALUES_FUNC_ARG_NAME, Add, + IndexConstant, IntermediateNode, Mul, Sub, @@ -161,3 +162,7 @@ class BaseTracer(ABC): # the order, we need to do as in __rmul__, ie mostly a copy of __mul__ + # some changes __rmul__ = __mul__ + + def __getitem__(self, item): + traced_computation = IndexConstant(self.output, item) + return self.__class__([self], traced_computation, 0) diff --git a/concrete/numpy/np_indexing_helpers.py b/concrete/numpy/np_indexing_helpers.py new file mode 100644 index 000000000..945f66412 --- /dev/null +++ b/concrete/numpy/np_indexing_helpers.py @@ -0,0 +1,59 @@ +"""Helpers for indexing with numpy values functionality.""" + +from typing import Any + +import numpy + + +def should_sanitize(indexing_element: Any) -> bool: + """Decide whether to sanitize an indexing element or not. + + Sanitizing in this context means converting supported numpy values into python values. + + Args: + indexing_element (Any): the indexing element to decide sanitization. + + Returns: + bool: True if indexing element should be sanitized otherwise False. + """ + + return isinstance(indexing_element, numpy.integer) or ( + isinstance(indexing_element, numpy.ndarray) + and issubclass(indexing_element.dtype.type, numpy.integer) + and indexing_element.shape == () + ) + + +def process_indexing_element(indexing_element: Any) -> Any: + """Process an indexing element. + + Processing in this context means converting supported numpy values into python values. + (if they are decided to be sanitized) + + Args: + indexing_element (Any): the indexing element to sanitize. + + Returns: + Any: the sanitized indexing element. + """ + + if isinstance(indexing_element, slice): + + start = indexing_element.start + if should_sanitize(start): + start = int(start) + + stop = indexing_element.stop + if should_sanitize(stop): + stop = int(stop) + + step = indexing_element.step + if should_sanitize(step): + step = int(step) + + indexing_element = slice(start, stop, step) + + elif should_sanitize(indexing_element): + indexing_element = int(indexing_element) + + return indexing_element diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index f8316dc89..dbb2f25b6 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -18,6 +18,7 @@ from .np_dtypes_helpers import ( get_base_value_for_numpy_or_python_constant_data, get_numpy_function_output_dtype, ) +from .np_indexing_helpers import process_indexing_element SUPPORTED_TYPES_FOR_TRACING = (int, float, numpy.ndarray) + tuple( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES @@ -264,6 +265,14 @@ class NPTracer(BaseTracer): ) return output_tracer + def __getitem__(self, item): + if isinstance(item, tuple): + item = tuple(process_indexing_element(indexing_element) for indexing_element in item) + else: + item = process_indexing_element(item) + + return BaseTracer.__getitem__(self, item) + # Supported functions are either univariate or bivariate for which one of the two # sources is a constant # diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index c20588f20..0228525e8 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -116,6 +116,54 @@ from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, En 20, id="Dot, np.array([1, 2, 3, 4]), np.array([4, 3, 2, 1])", ), + pytest.param( + ir.IndexConstant(EncryptedTensor(Integer(4, True), shape=(4,)), (0,)), + [ + numpy.array([1, 2, 3, 4], dtype=numpy.int32), + ], + 1, + id="IndexConstant, np.array([1, 2, 3, 4])[0]", + ), + pytest.param( + ir.IndexConstant(EncryptedTensor(Integer(4, True), shape=(4,)), (slice(1, 3, None),)), + [ + numpy.array([1, 2, 3, 4], dtype=numpy.int32), + ], + numpy.array([2, 3]), + id="IndexConstant, np.array([1, 2, 3, 4])[1:3]", + ), + pytest.param( + ir.IndexConstant(EncryptedTensor(Integer(4, True), shape=(4,)), (slice(3, 1, -1),)), + [ + numpy.array([1, 2, 3, 4], dtype=numpy.int32), + ], + numpy.array([4, 3], dtype=numpy.int32), + id="IndexConstant, np.array([1, 2, 3, 4])[3:1:-1]", + ), + pytest.param( + ir.IndexConstant( + EncryptedTensor(Integer(5, True), shape=(4, 4)), (slice(1, 3, 1), slice(2, 0, -1)) + ), + [ + numpy.array( + [ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + ], + dtype=numpy.int32, + ), + ], + numpy.array( + [ + [7, 6], + [11, 10], + ], + dtype=numpy.int32, + ), + id="IndexConstant, np.array([[1, 2, 3, 4]...[13, 14, 15, 16]])[1:3, 2:0:-1]", + ), ], ) def test_evaluate( @@ -124,7 +172,10 @@ def test_evaluate( expected_result: int, ): """Test evaluate methods on IntermediateNodes""" - assert node.evaluate(input_data) == expected_result + if isinstance(expected_result, numpy.ndarray): + assert (node.evaluate(input_data) == expected_result).all() + else: + assert node.evaluate(input_data) == expected_result @pytest.mark.parametrize( diff --git a/tests/conftest.py b/tests/conftest.py index c30fce90b..56cfd08bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from concrete.common.representation.intermediate import ( Add, Constant, Dot, + IndexConstant, Input, IntermediateNode, Mul, @@ -147,6 +148,15 @@ def is_equivalent_input(lhs: Input, rhs: object) -> bool: ) +def is_equivalent_index_constant(lhs: IndexConstant, rhs: object) -> bool: + """Helper function to check if an IndexConstant node is equivalent to an other object.""" + return ( + isinstance(rhs, IndexConstant) + and lhs.index == rhs.index + and is_equivalent_intermediate_node(lhs, rhs) + ) + + def is_equivalent_mul(lhs: Mul, rhs: object) -> bool: """Helper function to check if a Mul node is equivalent to an other object.""" return _is_equivalent_to_binary_commutative(lhs, rhs) @@ -171,6 +181,7 @@ EQUIVALENT_TEST_FUNC: Dict[Type, Callable[..., bool]] = { UnivariateFunction: is_equivalent_arbitrary_function, Constant: is_equivalent_constant, Dot: is_equivalent_dot, + IndexConstant: is_equivalent_index_constant, Input: is_equivalent_input, Mul: is_equivalent_mul, Sub: is_equivalent_sub, diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index bf583f13e..688875797 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -852,6 +852,20 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "return(%2)\n" ), ), + pytest.param( + lambda x: x[0], + {"x": EncryptedTensor(Integer(3, is_signed=True), shape=(2, 2))}, + [(numpy.random.randint(-4, 2 ** 2, size=(2, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%1 = IndexConstant(%0[0]) # EncryptedTensor, shape=(2,)>\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer tensor constant indexing is supported\n" # noqa: E501 # pylint: disable=line-too-long + "return(%1)\n" + ), + ), ], ) def test_fail_compile(function, parameters, inputset, match, default_compilation_configuration): diff --git a/tests/numpy/test_compile_constant_indexing.py b/tests/numpy/test_compile_constant_indexing.py new file mode 100644 index 000000000..78acd99ed --- /dev/null +++ b/tests/numpy/test_compile_constant_indexing.py @@ -0,0 +1,598 @@ +"""Test module for constant indexing.""" + +import numpy as np +import pytest + +from concrete.common.data_types import UnsignedInteger +from concrete.common.values import EncryptedScalar, EncryptedTensor +from concrete.numpy import compile_numpy_function_into_op_graph + + +@pytest.mark.parametrize( + "input_value,function_with_indexing,output_value", + [ + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-2], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-1], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3:], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-2:], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-1:], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1:], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2:], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:-2], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:2], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:3], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3:-2], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3:1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3:2], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3:3], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-2:-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-2:2], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-2:3], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-1:3], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:-2], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:2], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:3], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1:-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1:2], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1:3], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2:3], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[::-1], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-3::-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-2::-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-1::-1], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0::-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1::-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2::-1], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:-3:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:-2:-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:0:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:1:-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2:0:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2:1:-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-1:1:-1], + EncryptedTensor(UnsignedInteger(1), shape=(1,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[-1:0:-1], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[:, :, :], + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0, :, :], + EncryptedTensor(UnsignedInteger(1), shape=(4, 5)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[:, 0, :], + EncryptedTensor(UnsignedInteger(1), shape=(3, 5)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[:, :, 0], + EncryptedTensor(UnsignedInteger(1), shape=(3, 4)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0, 0, :], + EncryptedTensor(UnsignedInteger(1), shape=(5,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0, :, 0], + EncryptedTensor(UnsignedInteger(1), shape=(4,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[:, 0, 0], + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0:, 1:, 2:], + EncryptedTensor(UnsignedInteger(1), shape=(3, 3, 3)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[2:, 1:, 0:], + EncryptedTensor(UnsignedInteger(1), shape=(1, 3, 5)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0], + EncryptedTensor(UnsignedInteger(1), shape=(4, 5)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0, 0], + EncryptedTensor(UnsignedInteger(1), shape=(5,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3, 4, 5)), + lambda x: x[0, 0, 0], + EncryptedScalar(UnsignedInteger(1)), + ), + ], +) +def test_constant_indexing( + default_compilation_configuration, + input_value, + function_with_indexing, + output_value, +): + """Test compile_numpy_function_into_op_graph with constant indexing""" + + inputset = [ + ( + np.random.randint( + input_value.dtype.min_value(), + input_value.dtype.max_value() + 1, + size=input_value.shape, + ), + ) + for _ in range(10) + ] + + opgraph = compile_numpy_function_into_op_graph( + function_with_indexing, + {"x": input_value}, + inputset, + default_compilation_configuration, + ) + + assert len(opgraph.output_nodes) == 1 + output_node = opgraph.output_nodes[0] + + assert len(output_node.outputs) == 1 + assert output_value == output_node.outputs[0] + + +@pytest.mark.parametrize( + "input_value,function_with_indexing,expected_error_type,expected_error_message", + [ + pytest.param( + EncryptedScalar(UnsignedInteger(1)), + lambda x: x[0], + TypeError, + "Only tensors can be indexed " + "but you tried to index EncryptedScalar>", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0.5], + TypeError, + "Only integers and integer slices can be used for indexing " + "but you tried to use 0.5 for indexing", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[1:5:0.5], # type: ignore + TypeError, + "Only integers and integer slices can be used for indexing " + "but you tried to use 1:5:0.5 for indexing", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0, 1], + ValueError, + "Tensor of shape (3,) cannot be indexed with [0, 1] " + "as the index has more elements than the number of dimensions of the tensor", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[5], + ValueError, + "Tensor of shape (3,) cannot be indexed with [5] " + "because index is out of range for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[5:], + ValueError, + "Tensor of shape (3,) cannot be indexed with [5:] " + "because start index is out of range for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:10], + ValueError, + "Tensor of shape (3,) cannot be indexed with [:10] " + "because stop index is out of range for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[2:0], + ValueError, + "Tensor of shape (3,) cannot be indexed with [2:0] " + "because start index is not less than stop index for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[5::-1], + ValueError, + "Tensor of shape (3,) cannot be indexed with [5::-1] " + "because start index is out of range for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[:10:-1], + ValueError, + "Tensor of shape (3,) cannot be indexed with [:10:-1] " + "because stop index is out of range for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[0:2:-1], + ValueError, + "Tensor of shape (3,) cannot be indexed with [0:2:-1] " + "because step is negative and stop index is not less than start index for dimension 0", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[::0], + ValueError, + "Tensor of shape (3,) cannot be indexed with [::0] " + "because step is zero for dimension 0", + ), + ], +) +def test_invalid_constant_indexing( + default_compilation_configuration, + input_value, + function_with_indexing, + expected_error_type, + expected_error_message, +): + """Test compile_numpy_function_into_op_graph with invalid constant indexing""" + + with pytest.raises(expected_error_type): + try: + inputset = [ + ( + np.random.randint( + input_value.dtype.min_value(), + input_value.dtype.max_value() + 1, + size=input_value.shape, + ), + ) + for _ in range(10) + ] + compile_numpy_function_into_op_graph( + function_with_indexing, + {"x": input_value}, + inputset, + default_compilation_configuration, + ) + except Exception as error: + assert str(error) == expected_error_message + raise + + +@pytest.mark.parametrize( + "input_value,function_with_indexing,output_value", + [ + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[np.uint32(0)], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[slice(np.uint32(2), np.int32(0), np.int8(-1))], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[np.array(0)], + EncryptedScalar(UnsignedInteger(1)), + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[slice(np.array(2), np.array(0), np.array(-1))], + EncryptedTensor(UnsignedInteger(1), shape=(2,)), + ), + ], +) +def test_constant_indexing_with_numpy_integers( + default_compilation_configuration, + input_value, + function_with_indexing, + output_value, +): + """Test compile_numpy_function_into_op_graph with constant indexing with numpy integers""" + + inputset = [ + ( + np.random.randint( + input_value.dtype.min_value(), + input_value.dtype.max_value() + 1, + size=input_value.shape, + ), + ) + for _ in range(10) + ] + + opgraph = compile_numpy_function_into_op_graph( + function_with_indexing, + {"x": input_value}, + inputset, + default_compilation_configuration, + ) + + assert len(opgraph.output_nodes) == 1 + output_node = opgraph.output_nodes[0] + + assert len(output_node.outputs) == 1 + assert output_value == output_node.outputs[0] + + +@pytest.mark.parametrize( + "input_value,function_with_indexing,expected_error_type,expected_error_message", + [ + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[np.float32(1.5)], + TypeError, + "Only integers and integer slices can be used for indexing " + "but you tried to use 1.5 for indexing", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[np.array(1.5)], + TypeError, + "Only integers and integer slices can be used for indexing " + "but you tried to use 1.5 for indexing", + ), + pytest.param( + EncryptedTensor(UnsignedInteger(1), shape=(3,)), + lambda x: x[np.array([1, 2])], + TypeError, + "Only integers and integer slices can be used for indexing " + "but you tried to use [1 2] for indexing", + ), + ], +) +def test_invalid_constant_indexing_with_numpy_values( + default_compilation_configuration, + input_value, + function_with_indexing, + expected_error_type, + expected_error_message, +): + """Test compile_numpy_function_into_op_graph with invalid constant indexing with numpy values""" + + with pytest.raises(expected_error_type): + try: + inputset = [ + ( + np.random.randint( + input_value.dtype.min_value(), + input_value.dtype.max_value() + 1, + size=input_value.shape, + ), + ) + for _ in range(10) + ] + compile_numpy_function_into_op_graph( + function_with_indexing, + {"x": input_value}, + inputset, + default_compilation_configuration, + ) + except Exception as error: + assert str(error) == expected_error_message + raise From c46d96aabf9ceb6914255c7a5032d8cee65b9bea Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 25 Oct 2021 17:19:35 +0300 Subject: [PATCH 0464/1104] refactor(compilation): improve error messages of indexing --- concrete/common/mlir/utils.py | 3 +-- tests/numpy/test_compile.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 4dcc60d86..cc75113c0 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -73,8 +73,7 @@ def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) elif isinstance(node, intermediate.IndexConstant): # constraints for constant indexing assert_true(len(outputs) == 1) - if not value_is_unsigned_integer(outputs[0]): - return "only unsigned integer tensor constant indexing is supported" + return "indexing is not supported for the time being" else: # pragma: no cover assert_not_reached("Non IntermediateNode object in the OPGraph") diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 688875797..95a76b6a7 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -862,7 +862,7 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long "%1 = IndexConstant(%0[0]) # EncryptedTensor, shape=(2,)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer tensor constant indexing is supported\n" # noqa: E501 # pylint: disable=line-too-long + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ indexing is not supported for the time being\n" # noqa: E501 # pylint: disable=line-too-long "return(%1)\n" ), ), From 624143106f9e5ac300434f07019daef29378d57b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Mon, 25 Oct 2021 12:06:24 +0200 Subject: [PATCH 0465/1104] refactor(compilation): remove unnecessary check in compile.py refs #645 --- concrete/numpy/compile.py | 13 +- .../common/compilation/test_configuration.py | 2 - tests/numpy/test_compile.py | 137 ++++++++++-------- 3 files changed, 79 insertions(+), 73 deletions(-) diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 59c6fc1a5..39ebe40f6 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -2,7 +2,7 @@ import sys import traceback -from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, Optional, Tuple import numpy from zamalang import CompilerEngine @@ -21,7 +21,6 @@ from ..common.mlir.utils import ( ) from ..common.operator_graph import OPGraph from ..common.optimization.topological import fuse_float_operations -from ..common.representation.intermediate import IntermediateNode from ..common.values import BaseValue from ..numpy.tracing import trace_numpy_function from .np_dtypes_helpers import ( @@ -102,16 +101,6 @@ def _compile_numpy_function_into_op_graph_internal( if not check_op_graph_is_integer_program(op_graph): fuse_float_operations(op_graph, compilation_artifacts) - # TODO: To be removed once we support more than integers - offending_non_integer_nodes: List[IntermediateNode] = [] - op_grap_is_int_prog = check_op_graph_is_integer_program(op_graph, offending_non_integer_nodes) - if not op_grap_is_int_prog: - raise ValueError( - f"{function_to_compile.__name__} cannot be compiled as it has nodes with either float" - f" inputs or outputs.\nOffending nodes : " - f"{', '.join(str(node) for node in offending_non_integer_nodes)}" - ) - # Find bounds with the inputset inputset_size, node_bounds_and_samples = eval_op_graph_bounds_on_inputset( op_graph, diff --git a/tests/common/compilation/test_configuration.py b/tests/common/compilation/test_configuration.py index daadbd307..454807a56 100644 --- a/tests/common/compilation/test_configuration.py +++ b/tests/common/compilation/test_configuration.py @@ -35,8 +35,6 @@ def simple_fuse_not_output(x): simple_fuse_not_output, True, id="simple_fuse_not_output", - marks=pytest.mark.xfail(strict=True), - # fails because it connot be compiled without topological optimizations ), ], ) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 95a76b6a7..95a21786e 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -448,12 +448,6 @@ def test_unary_ufunc_operations(ufunc, default_compilation_configuration): ((4, 8), (3, 4), (0, 4)), ["x", "y", "z"], ), - pytest.param( - no_fuse_unhandled, - ((-2, 2), (-2, 2)), - ["x", "y"], - marks=pytest.mark.xfail(strict=True, raises=ValueError), - ), pytest.param(complicated_topology, ((0, 10),), ["x"]), ], ) @@ -735,6 +729,7 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ) +# pylint: disable=line-too-long,unnecessary-lambda @pytest.mark.parametrize( "function,parameters,inputset,match", [ @@ -745,10 +740,10 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 + "%1 = x # EncryptedScalar>\n" # noqa: E501 + "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported\n" # noqa: E501 "return(%2)\n" ), ), @@ -759,10 +754,10 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = Constant(-1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = x # EncryptedScalar>\n" # noqa: E501 + "%1 = Constant(-1) # ClearScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 + "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 "return(%2)\n" ), ), @@ -773,10 +768,10 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 + "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 "return(%2)\n" ), ), @@ -787,10 +782,10 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 + "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 "return(%2)\n" ), ), @@ -801,10 +796,10 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Mul(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar multiplication is supported\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 + "%2 = Mul(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar multiplication is supported\n" # noqa: E501 "return(%2)\n" ), ), @@ -815,15 +810,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = Constant(127) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Sub(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar subtraction is supported\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = Constant(127) # ClearScalar>\n" # noqa: E501 + "%1 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "%2 = Sub(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar subtraction is supported\n" # noqa: E501 "return(%2)\n" ), ), pytest.param( - lambda x, y: numpy.dot(x, y), # pylint: disable=unnecessary-lambda + lambda x, y: numpy.dot(x, y), { "x": EncryptedTensor(Integer(2, is_signed=True), shape=(1,)), "y": EncryptedTensor(Integer(2, is_signed=True), shape=(1,)), @@ -843,12 +838,12 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = x # EncryptedTensor, shape=(1,)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = y # EncryptedTensor, shape=(1,)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = Dot(%0, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer dot product is supported\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = x # EncryptedTensor, shape=(1,)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 + "%1 = y # EncryptedTensor, shape=(1,)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 + "%2 = Dot(%0, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer dot product is supported\n" # noqa: E501 "return(%2)\n" ), ), @@ -866,22 +861,44 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "return(%1)\n" ), ), + pytest.param( + no_fuse_unhandled, + {"x": EncryptedScalar(Integer(2, False)), "y": EncryptedScalar(Integer(2, False))}, + [(i, i) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n\n" + "%0 = x # EncryptedScalar>\n" # noqa: E501 + "%1 = Constant(2.8) # ClearScalar>\n" + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 + "%2 = y # EncryptedScalar>\n" # noqa: E501 + "%3 = Constant(9.3) # ClearScalar>\n" + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 + "%4 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "%5 = Add(%2, %3) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "%6 = Add(%4, %5) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "%7 = astype(int32)(%6) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 + "return(%7)\n" + ), + ), ], ) +# pylint: enable=line-too-long,unnecessary-lambda def test_fail_compile(function, parameters, inputset, match, default_compilation_configuration): """Test function compile_numpy_function_into_op_graph for a program with signed values""" - with pytest.raises(RuntimeError): - try: - compile_numpy_function( - function, - parameters, - inputset, - default_compilation_configuration, - ) - except RuntimeError as error: - assert str(error) == match - raise + with pytest.raises(RuntimeError) as excinfo: + compile_numpy_function( + function, + parameters, + inputset, + default_compilation_configuration, + ) + + assert str(excinfo.value) == match def test_fail_with_intermediate_signed_values(default_compilation_configuration): @@ -905,22 +922,24 @@ def test_fail_with_intermediate_signed_values(default_compilation_configuration) show_mlir=True, ) except RuntimeError as error: + # pylint: disable=line-too-long match = ( "function you are trying to compile isn't supported for MLIR lowering\n" "\n" - "%0 = y # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = Constant(10) # ClearScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%2 = x # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%3 = np.negative(%2) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%4 = Mul(%3, %1) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%5 = np.absolute(%4) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%6 = astype(int32)(%5) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long - "%7 = Add(%6, %0) # EncryptedScalar>\n" # noqa: E501 # pylint: disable=line-too-long + "%0 = y # EncryptedScalar>\n" # noqa: E501 + "%1 = Constant(10) # ClearScalar>\n" # noqa: E501 + "%2 = x # EncryptedScalar>\n" # noqa: E501 + "%3 = np.negative(%2) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "%4 = Mul(%3, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "%5 = np.absolute(%4) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 + "%6 = astype(int32)(%5) # EncryptedScalar>\n" # noqa: E501 + "%7 = Add(%6, %0) # EncryptedScalar>\n" # noqa: E501 "return(%7)\n" ) + # pylint: enable=line-too-long assert str(error) == match raise From 9459675cfb211c3765dc52a2e4548d53b3413dfc Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Fri, 22 Oct 2021 14:43:34 +0200 Subject: [PATCH 0466/1104] feat: manage signed constants closes #688 closes #612 --- concrete/common/data_types/dtypes_helpers.py | 13 ++++ concrete/common/mlir/converters.py | 21 +++--- concrete/common/mlir/utils.py | 13 ++-- tests/common/mlir/test_converters.py | 8 --- tests/numpy/test_compile.py | 70 ++++++++++++++------ 5 files changed, 80 insertions(+), 45 deletions(-) diff --git a/concrete/common/data_types/dtypes_helpers.py b/concrete/common/data_types/dtypes_helpers.py index 94f43ead0..add842be9 100644 --- a/concrete/common/data_types/dtypes_helpers.py +++ b/concrete/common/data_types/dtypes_helpers.py @@ -83,6 +83,19 @@ def value_is_scalar(value_to_check: BaseValue) -> bool: return isinstance(value_to_check, TensorValue) and value_to_check.is_scalar +def value_is_integer(value_to_check: BaseValue) -> bool: + """Check that a value is of type Integer. + + Args: + value_to_check (BaseValue): The value to check + + Returns: + bool: True if the passed value_to_check is of type Integer + """ + + return isinstance(value_to_check.dtype, INTEGER_TYPES) + + def value_is_unsigned_integer(value_to_check: BaseValue) -> bool: """Check that a value is of type Integer and is unsigned. diff --git a/concrete/common/mlir/converters.py b/concrete/common/mlir/converters.py index fe691c1d6..569fa84ad 100644 --- a/concrete/common/mlir/converters.py +++ b/concrete/common/mlir/converters.py @@ -17,6 +17,7 @@ from zamalang.dialects import hlfhe from ..data_types.dtypes_helpers import ( value_is_clear_scalar_integer, value_is_clear_tensor_integer, + value_is_encrypted_scalar_integer, value_is_encrypted_scalar_unsigned_integer, value_is_encrypted_tensor_integer, ) @@ -30,18 +31,18 @@ def add(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert an addition intermediate node.""" assert_true(len(node.inputs) == 2, "addition should have two inputs") assert_true(len(node.outputs) == 1, "addition should have a single output") - if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( + if value_is_encrypted_scalar_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): return _add_eint_int(node, preds, ir_to_mlir_node, ctx) - if value_is_encrypted_scalar_unsigned_integer(node.inputs[1]) and value_is_clear_scalar_integer( + if value_is_encrypted_scalar_integer(node.inputs[1]) and value_is_clear_scalar_integer( node.inputs[0] ): # flip lhs and rhs return _add_eint_int(node, preds[::-1], ir_to_mlir_node, ctx) - if value_is_encrypted_scalar_unsigned_integer( - node.inputs[0] - ) and value_is_encrypted_scalar_unsigned_integer(node.inputs[1]): + if value_is_encrypted_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_integer( + node.inputs[1] + ): return _add_eint_eint(node, preds, ir_to_mlir_node, ctx) raise TypeError( f"Don't support addition between {str(node.inputs[0])} and {str(node.inputs[1])}" @@ -74,7 +75,7 @@ def sub(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert a subtraction intermediate node.""" assert_true(len(node.inputs) == 2, "subtraction should have two inputs") assert_true(len(node.outputs) == 1, "subtraction should have a single output") - if value_is_clear_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_unsigned_integer( + if value_is_clear_scalar_integer(node.inputs[0]) and value_is_encrypted_scalar_integer( node.inputs[1] ): return _sub_int_eint(node, preds, ir_to_mlir_node, ctx) @@ -98,11 +99,11 @@ def mul(node, preds, ir_to_mlir_node, ctx, _additional_conversion_info=None): """Convert a multiplication intermediate node.""" assert_true(len(node.inputs) == 2, "multiplication should have two inputs") assert_true(len(node.outputs) == 1, "multiplication should have a single output") - if value_is_encrypted_scalar_unsigned_integer(node.inputs[0]) and value_is_clear_scalar_integer( + if value_is_encrypted_scalar_integer(node.inputs[0]) and value_is_clear_scalar_integer( node.inputs[1] ): return _mul_eint_int(node, preds, ir_to_mlir_node, ctx) - if value_is_encrypted_scalar_unsigned_integer(node.inputs[1]) and value_is_clear_scalar_integer( + if value_is_encrypted_scalar_integer(node.inputs[1]) and value_is_clear_scalar_integer( node.inputs[0] ): # flip lhs and rhs @@ -131,8 +132,6 @@ def constant(node, _preds, _ir_to_mlir_node, ctx, _additional_conversion_info=No value = cast(TensorValue, value) dtype = cast(Integer, value.dtype) - if dtype.is_signed: - raise TypeError("Don't support signed constant integer") data = node.constant_data int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) @@ -142,8 +141,6 @@ def constant(node, _preds, _ir_to_mlir_node, ctx, _additional_conversion_info=No value = cast(TensorValue, value) dtype = cast(Integer, value.dtype) - if dtype.is_signed: - raise TypeError("Don't support signed constant integer tensor") data = node.constant_data int_type = IntegerType.get_signless(dtype.bit_width, context=ctx) diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index cc75113c0..b46d9e355 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -7,6 +7,7 @@ from ..data_types.dtypes_helpers import ( value_is_clear_tensor_integer, value_is_encrypted_scalar_integer, value_is_encrypted_tensor_integer, + value_is_integer, value_is_scalar, value_is_unsigned_integer, ) @@ -58,8 +59,10 @@ def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) elif isinstance(node, intermediate.Constant): # constraints for constants assert_true(len(outputs) == 1) - if not value_is_unsigned_integer(outputs[0]): - return "only unsigned integer constants are supported" + # We currently can't fail on the following assert, but let it for possible changes in the + # future + if not value_is_integer(outputs[0]): + return "only integer constants are supported" # pragma: no cover elif isinstance(node, intermediate.UnivariateFunction): # constraints for univariate functions assert_true(len(inputs) == 1) @@ -84,8 +87,10 @@ def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) return "only scalar unsigned integer outputs are supported" else: for out in outputs: - if not value_is_unsigned_integer(out): - return "only unsigned integer intermediates are supported" + # We currently can't fail on the following assert, but let it for possible changes in + # the future + if not value_is_integer(out): + return "only integer intermediates are supported" # pragma: no cover # pylint: enable=too-many-branches,too-many-return-statements diff --git a/tests/common/mlir/test_converters.py b/tests/common/mlir/test_converters.py index d9f590669..8a292e6b5 100644 --- a/tests/common/mlir/test_converters.py +++ b/tests/common/mlir/test_converters.py @@ -37,14 +37,6 @@ def test_fail_non_integer_const(): constant(MockNode(outputs=[ClearTensor(Float(32), shape=(2,))]), None, None, None) -def test_fail_signed_integer_const(): - """Test failing constant converter with non-integer""" - with pytest.raises(TypeError, match=r"Don't support signed constant integer"): - constant(MockNode(outputs=[ClearScalar(Integer(8, True))]), None, None, None) - with pytest.raises(TypeError, match=r"Don't support signed constant integer tensor"): - constant(MockNode(outputs=[ClearTensor(Integer(8, True), shape=(2,))]), None, None, None) - - @pytest.mark.parametrize( "input_node", [ diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 95a21786e..1f134850d 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -482,6 +482,15 @@ def test_compile_function_multiple_outputs( @pytest.mark.parametrize( "function,input_ranges,list_of_arg_names", [ + pytest.param(lambda x: (-27) + 4 * (x + 8), ((0, 10),), ["x"]), + pytest.param(lambda x: x + (-33), ((40, 60),), ["x"]), + pytest.param(lambda x: 17 - (0 - x), ((0, 10),), ["x"]), + pytest.param(lambda x: 42 + x * (-3), ((0, 10),), ["x"]), + pytest.param(lambda x: 43 + (-4) * x, ((0, 10),), ["x"]), + pytest.param(lambda x: 3 - (-5) * x, ((0, 10),), ["x"]), + pytest.param(lambda x: (-2) * (-5) * x, ((0, 10),), ["x"]), + pytest.param(lambda x: (-2) * x * (-5), ((0, 10),), ["x"]), + pytest.param(lambda x, y: 40 - (-3 * x) + (-2 * y), ((0, 20), (0, 20)), ["x", "y"]), pytest.param(lambda x: x + numpy.int32(42), ((0, 10),), ["x"]), pytest.param(lambda x: x + 64, ((0, 10),), ["x"]), pytest.param(lambda x: x * 3, ((0, 40),), ["x"]), @@ -747,20 +756,6 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "return(%2)\n" ), ), - pytest.param( - lambda x: x + (-1), - {"x": EncryptedScalar(Integer(4, is_signed=False))}, - [(i,) for i in range(1, 2 ** 4)], - ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = x # EncryptedScalar>\n" # noqa: E501 - "%1 = Constant(-1) # ClearScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 - "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "return(%2)\n" - ), - ), pytest.param( lambda x: x + 1, {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, @@ -869,16 +864,16 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "function you are trying to compile isn't supported for MLIR lowering\n\n" "%0 = x # EncryptedScalar>\n" # noqa: E501 "%1 = Constant(2.8) # ClearScalar>\n" - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported\n" # noqa: E501 "%2 = y # EncryptedScalar>\n" # noqa: E501 "%3 = Constant(9.3) # ClearScalar>\n" - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer constants are supported\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported\n" # noqa: E501 "%4 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported\n" # noqa: E501 "%5 = Add(%2, %3) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported\n" # noqa: E501 "%6 = Add(%4, %5) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported\n" # noqa: E501 "%7 = astype(int32)(%6) # EncryptedScalar>\n" # noqa: E501 "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 "return(%7)\n" @@ -930,9 +925,7 @@ def test_fail_with_intermediate_signed_values(default_compilation_configuration) "%1 = Constant(10) # ClearScalar>\n" # noqa: E501 "%2 = x # EncryptedScalar>\n" # noqa: E501 "%3 = np.negative(%2) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 "%4 = Mul(%3, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer intermediates are supported\n" # noqa: E501 "%5 = np.absolute(%4) # EncryptedScalar>\n" # noqa: E501 "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 "%6 = astype(int32)(%5) # EncryptedScalar>\n" # noqa: E501 @@ -1103,3 +1096,38 @@ def test_compile_too_high_bitwidth(default_compilation_configuration): data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), default_compilation_configuration, ) + + +def test_failure_for_signed_output(default_compilation_configuration): + """Test that we don't accept signed output""" + function = lambda x: x + (-3) # pylint: disable=unnecessary-lambda # noqa: E731 + input_ranges = ((0, 10),) + list_of_arg_names = ["x"] + + def data_gen(args): + for prod in itertools.product(*args): + yield prod + + function_parameters = { + arg_name: EncryptedScalar(Integer(64, False)) for arg_name in list_of_arg_names + } + + with pytest.raises(RuntimeError) as excinfo: + compile_numpy_function( + function, + function_parameters, + data_gen(tuple(range(x[0], x[1] + 1) for x in input_ranges)), + default_compilation_configuration, + ) + + # pylint: disable=line-too-long + assert ( + str(excinfo.value) + == "function you are trying to compile isn't supported for MLIR lowering\n\n" # noqa: E501 + "%0 = x # EncryptedScalar>\n" # noqa: E501 + "%1 = Constant(-3) # ClearScalar>\n" # noqa: E501 + "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported\n" # noqa: E501 + "return(%2)\n" + ) + # pylint: enable=line-too-long From 70fbac7188ff96b05841c725bcb53ca993c6a34f Mon Sep 17 00:00:00 2001 From: Umut Date: Fri, 22 Oct 2021 16:17:15 +0300 Subject: [PATCH 0467/1104] feat(compilation): provide a way to automatically generate a random inputset --- concrete/common/compilation/configuration.py | 6 + concrete/numpy/compile.py | 33 ++-- concrete/numpy/np_inputset_helpers.py | 157 +++++++++++++++++++ tests/numpy/test_compile.py | 59 ++++++- tests/numpy/test_np_inputset_helpers.py | 96 ++++++++++++ 5 files changed, 341 insertions(+), 10 deletions(-) create mode 100644 concrete/numpy/np_inputset_helpers.py create mode 100644 tests/numpy/test_np_inputset_helpers.py diff --git a/concrete/common/compilation/configuration.py b/concrete/common/compilation/configuration.py index ad3bf1f86..0fd1233cf 100644 --- a/concrete/common/compilation/configuration.py +++ b/concrete/common/compilation/configuration.py @@ -8,6 +8,8 @@ class CompilationConfiguration: enable_topological_optimizations: bool check_every_input_in_inputset: bool treat_warnings_as_errors: bool + enable_unsafe_features: bool + random_inputset_samples: int def __init__( self, @@ -15,8 +17,12 @@ class CompilationConfiguration: enable_topological_optimizations: bool = True, check_every_input_in_inputset: bool = False, treat_warnings_as_errors: bool = False, + enable_unsafe_features: bool = False, + random_inputset_samples: int = 30, ): self.dump_artifacts_on_unexpected_failures = dump_artifacts_on_unexpected_failures self.enable_topological_optimizations = enable_topological_optimizations self.check_every_input_in_inputset = check_every_input_in_inputset self.treat_warnings_as_errors = treat_warnings_as_errors + self.enable_unsafe_features = enable_unsafe_features + self.random_inputset_samples = random_inputset_samples diff --git a/concrete/numpy/compile.py b/concrete/numpy/compile.py index 39ebe40f6..5e2d17de8 100644 --- a/concrete/numpy/compile.py +++ b/concrete/numpy/compile.py @@ -2,7 +2,7 @@ import sys import traceback -from typing import Any, Callable, Dict, Iterable, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, Optional, Tuple, Union import numpy from zamalang import CompilerEngine @@ -28,6 +28,7 @@ from .np_dtypes_helpers import ( get_base_value_for_numpy_or_python_constant_data, get_constructor_for_numpy_or_python_constant_data, ) +from .np_inputset_helpers import _check_special_inputset_availability, _generate_random_inputset from .np_mlir_converter import NPMLIRConverter @@ -158,7 +159,7 @@ def _compile_numpy_function_into_op_graph_internal( def compile_numpy_function_into_op_graph( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - inputset: Iterable[Tuple[Any, ...]], + inputset: Union[Iterable[Tuple[Any, ...]], str], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, ) -> OPGraph: @@ -168,9 +169,11 @@ def compile_numpy_function_into_op_graph( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterable on tuples which are of the same length than the number of - parameters in the function, and in the same order than these same parameters + inputset (Union[Iterable[Tuple[Any, ...]], str]): The inputset over which op_graph + is evaluated. It needs to be an iterable on tuples which are of the same length than + the number of parameters in the function, and in the same order than these same + parameters. Alternatively, it can be "random" but that's an unstable feature and should + not be used in production. compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use during compilation compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill @@ -191,6 +194,11 @@ def compile_numpy_function_into_op_graph( if compilation_artifacts is None: compilation_artifacts = CompilationArtifacts() + # Generate random inputset if it is requested and available + if isinstance(inputset, str): + _check_special_inputset_availability(inputset, compilation_configuration) + inputset = _generate_random_inputset(function_parameters, compilation_configuration) + # Try to compile the function and save partial artifacts on failure try: # Use context manager to restore numpy error handling @@ -306,7 +314,7 @@ def _compile_numpy_function_internal( def compile_numpy_function( function_to_compile: Callable, function_parameters: Dict[str, BaseValue], - inputset: Iterable[Tuple[Any, ...]], + inputset: Union[Iterable[Tuple[Any, ...]], str], compilation_configuration: Optional[CompilationConfiguration] = None, compilation_artifacts: Optional[CompilationArtifacts] = None, show_mlir: bool = False, @@ -317,9 +325,11 @@ def compile_numpy_function( function_to_compile (Callable): The function to compile function_parameters (Dict[str, BaseValue]): A dictionary indicating what each input of the function is e.g. an EncryptedScalar holding a 7bits unsigned Integer - inputset (Iterable[Tuple[Any, ...]]): The inputset over which op_graph is evaluated. It - needs to be an iterable on tuples which are of the same length than the number of - parameters in the function, and in the same order than these same parameters + inputset (Union[Iterable[Tuple[Any, ...]], str]): The inputset over which op_graph + is evaluated. It needs to be an iterable on tuples which are of the same length than + the number of parameters in the function, and in the same order than these same + parameters. Alternatively, it can be "random" but that's an unstable feature and should + not be used in production. compilation_configuration (Optional[CompilationConfiguration]): Configuration object to use during compilation compilation_artifacts (Optional[CompilationArtifacts]): Artifacts object to fill @@ -342,6 +352,11 @@ def compile_numpy_function( if compilation_artifacts is None: compilation_artifacts = CompilationArtifacts() + # Generate random inputset if it is requested and available + if isinstance(inputset, str): + _check_special_inputset_availability(inputset, compilation_configuration) + inputset = _generate_random_inputset(function_parameters, compilation_configuration) + # Try to compile the function and save partial artifacts on failure try: # Use context manager to restore numpy error handling diff --git a/concrete/numpy/np_inputset_helpers.py b/concrete/numpy/np_inputset_helpers.py new file mode 100644 index 000000000..d7a85e141 --- /dev/null +++ b/concrete/numpy/np_inputset_helpers.py @@ -0,0 +1,157 @@ +"""Helpers for numpy inputset related functionality.""" + +import random +from typing import Any, Dict, Iterable, Tuple + +import numpy + +from ..common.compilation import CompilationConfiguration +from ..common.data_types import Float, Integer +from ..common.values import BaseValue, TensorValue + + +def _generate_random_integer_scalar(dtype: Integer) -> int: + """Generate a random integer scalar. + + Args: + dtype (Integer): the data type to extract bounds + + Returns: + int: a random value within the range [dtype.min_value(), dtype.max_value()] + """ + + return random.randint(dtype.min_value(), dtype.max_value()) + + +def _generate_random_integer_tensor(dtype: Integer, shape: Tuple[int, ...]) -> numpy.ndarray: + """Generate a random integer tensor. + + Args: + dtype (Integer): the data type to extract bounds + shape (Tuple[int, ...]): the shape of the generated tensor + + Returns: + numpy.ndarray: a random array of the specified shape where each value of it + is within the range [dtype.min_value(), dtype.max_value()] + """ + + return numpy.random.randint( + dtype.min_value(), + dtype.max_value() + 1, + size=shape, + dtype=numpy.int64 if dtype.is_signed else numpy.uint64, # type: ignore + ) + + +def _generate_random_float_scalar() -> float: + """Generate a random float scalar. + + Returns: + float: a random value within the range [0, 1) + """ + + return random.random() + + +def _generate_random_float_tensor(dtype: Float, shape: Tuple[int, ...]) -> numpy.ndarray: + """Generate a random float tensor. + + Args: + dtype (Integer): the data type to extract resulting numpy data type + shape (Tuple[int, ...]): the shape of the generated tensor + + Returns: + numpy.ndarray: a random array of the specified shape where each value of it + is within the range [0, 1) + """ + + result = numpy.random.rand(*shape) + return result.astype(numpy.float32 if dtype.bit_width == 32 else numpy.float64) + + +def _generate_random_inputset( + function_parameters: Dict[str, BaseValue], + compilation_configuration: CompilationConfiguration, +) -> Iterable[Tuple[Any, ...]]: + """Generate a random inputset from function parameters. + + Using this function is not a good practice since the randomly generated inputset + might not reflect real world data. We have it to speed up our development workflow + and we also don't use it in any of our tests, benchmarks, or examples. + + Args: + function_parameters (Dict[str, BaseValue]): the function parameters + to extract data types and shapes + compilation_configuration (CompilationConfiguration): the compilation configuration + to extract the sample size of the resulting inputset + + Raises: + ValueError: if the provided function arguments cannot be used for random inputset generation + + Returns: + None + """ + + inputset = [] + for _ in range(compilation_configuration.random_inputset_samples): + sample = [] + for parameter in function_parameters.values(): + if not isinstance(parameter, TensorValue): + raise ValueError(f"Random inputset cannot be generated for {parameter} parameters") + + if isinstance(parameter.dtype, Integer): + sample.append( + _generate_random_integer_scalar(parameter.dtype) + if parameter.is_scalar + else _generate_random_integer_tensor(parameter.dtype, parameter.shape) + ) + elif isinstance(parameter.dtype, Float): + sample.append( + _generate_random_float_scalar() + if parameter.is_scalar + else _generate_random_float_tensor(parameter.dtype, parameter.shape) + ) + else: + raise ValueError( + f"Random inputset cannot be generated " + f"for parameters of type {parameter.dtype}" + ) + inputset.append(tuple(sample)) + return inputset + + +def _check_special_inputset_availability( + inputset: str, + compilation_configuration: CompilationConfiguration, +): + """Check special inputset is valid and is available. + + This function makes sure the provided special inputset is valid and can be used with the + provided compilation configuration. + + Currently, the only special inputset is "random" but this can be extended in the future. + + Args: + inputset (str): the special inputset to check + compilation_configuration (CompilationConfiguration): the compilation configuration + to check the availability of the provided special inputset + + Raises: + ValueError: if the provided special inputset is not valid + RuntimeError: if the provided special inputset is not available + + Returns: + None + """ + + if inputset != "random": + raise ValueError( + f"inputset can only be an iterable of tuples or the string 'random' " + f"but you specified '{inputset}' for it" + ) + + if not compilation_configuration.enable_unsafe_features: + raise RuntimeError( + "Random inputset generation is an unsafe feature and should not be used " + "if you don't know what you are doing" + ) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 1f134850d..2be41db05 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -1,12 +1,13 @@ """Test file for numpy compilation functions""" import itertools import random +from copy import deepcopy import numpy import pytest from concrete.common.compilation import CompilationConfiguration -from concrete.common.data_types.integers import Integer +from concrete.common.data_types.integers import Integer, UnsignedInteger from concrete.common.debugging import draw_graph, get_printable_graph from concrete.common.extensions.table import LookupTable from concrete.common.values import ClearTensor, EncryptedScalar, EncryptedTensor @@ -1131,3 +1132,59 @@ def test_failure_for_signed_output(default_compilation_configuration): "return(%2)\n" ) # pylint: enable=line-too-long + + +def test_compile_with_random_inputset(default_compilation_configuration): + """Test function for compile with random input set""" + + configuration_to_use = deepcopy(default_compilation_configuration) + configuration_to_use.enable_unsafe_features = True + + compile_numpy_function_into_op_graph( + lambda x: x + 1, + {"x": EncryptedScalar(UnsignedInteger(6))}, + inputset="random", + compilation_configuration=configuration_to_use, + ) + compile_numpy_function( + lambda x: x + 32, + {"x": EncryptedScalar(UnsignedInteger(6))}, + inputset="random", + compilation_configuration=configuration_to_use, + ) + + +def test_fail_compile_with_random_inputset(default_compilation_configuration): + """Test function for failed compile with random input set""" + + with pytest.raises(ValueError): + try: + compile_numpy_function_into_op_graph( + lambda x: x + 1, + {"x": EncryptedScalar(UnsignedInteger(3))}, + inputset="unsupported", + compilation_configuration=default_compilation_configuration, + ) + except Exception as error: + expected = ( + "inputset can only be an iterable of tuples or the string 'random' " + "but you specified 'unsupported' for it" + ) + assert str(error) == expected + raise + + with pytest.raises(RuntimeError): + try: + compile_numpy_function( + lambda x: x + 1, + {"x": EncryptedScalar(UnsignedInteger(3))}, + inputset="random", + compilation_configuration=default_compilation_configuration, + ) + except Exception as error: + expected = ( + "Random inputset generation is an unsafe feature " + "and should not be used if you don't know what you are doing" + ) + assert str(error) == expected + raise diff --git a/tests/numpy/test_np_inputset_helpers.py b/tests/numpy/test_np_inputset_helpers.py new file mode 100644 index 000000000..37eb25e2f --- /dev/null +++ b/tests/numpy/test_np_inputset_helpers.py @@ -0,0 +1,96 @@ +"""Test file for numpy inputset helpers""" + +import numpy as np +import pytest + +from concrete.common.compilation import CompilationConfiguration +from concrete.common.data_types import Float, UnsignedInteger +from concrete.common.data_types.base import BaseDataType +from concrete.common.values import BaseValue, EncryptedScalar, EncryptedTensor +from concrete.numpy.np_inputset_helpers import _generate_random_inputset + + +def test_generate_random_inputset(): + """Test function for generate_random_inputset""" + + inputset = _generate_random_inputset( + { + "x1": EncryptedScalar(UnsignedInteger(4)), + "x2": EncryptedTensor(UnsignedInteger(4), shape=(2, 3)), + "x3": EncryptedScalar(Float(64)), + "x4": EncryptedTensor(Float(64), shape=(3, 2)), + }, + CompilationConfiguration(random_inputset_samples=15), + ) + + assert isinstance(inputset, list) + assert len(inputset) == 15 + + for sample in inputset: + assert isinstance(sample, tuple) + assert len(sample) == 4 + + assert isinstance(sample[0], int) + assert 0 <= sample[0] < 2 ** 4 + + assert isinstance(sample[1], np.ndarray) + assert sample[1].dtype == np.uint64 + assert sample[1].shape == (2, 3) + assert (sample[1] >= 0).all() + assert (sample[1] < 2 ** 4).all() + + assert isinstance(sample[2], float) + assert 0 <= sample[2] < 1 + + assert isinstance(sample[3], np.ndarray) + assert sample[3].dtype == np.float64 + assert sample[3].shape == (3, 2) + assert (sample[3] >= 0).all() + assert (sample[3] < 1).all() + + +def test_fail_generate_random_inputset(): + """Test function for failed generate_random_inputset""" + + class MockDtype(BaseDataType): + """Unsupported dtype to check error messages""" + + def __eq__(self, o: object) -> bool: + return False + + def __str__(self): + return "MockDtype" + + class MockValue(BaseValue): + """Unsupported value to check error messages""" + + def __init__(self): + super().__init__(MockDtype(), is_encrypted=True) + + def __eq__(self, other: object) -> bool: + return False + + def __str__(self): + return "MockValue" + + with pytest.raises(ValueError): + try: + _generate_random_inputset( + {"x": MockValue()}, + CompilationConfiguration(random_inputset_samples=15), + ) + except Exception as error: + expected = "Random inputset cannot be generated for MockValue parameters" + assert str(error) == expected + raise + + with pytest.raises(ValueError): + try: + _generate_random_inputset( + {"x": EncryptedScalar(MockDtype())}, + CompilationConfiguration(random_inputset_samples=15), + ) + except Exception as error: + expected = "Random inputset cannot be generated for parameters of type MockDtype" + assert str(error) == expected + raise From ecfde7b233638db500bce4c0ccc8f1a1a1a6f5d1 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 26 Oct 2021 15:06:08 +0200 Subject: [PATCH 0468/1104] refactor(debugging): accept several highlights per node when printing refs #645 --- concrete/common/debugging/printing.py | 16 +++++++++------- concrete/common/mlir/utils.py | 12 ++++++------ tests/common/debugging/test_printing.py | 6 ++++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/concrete/common/debugging/printing.py b/concrete/common/debugging/printing.py index cfca8696b..13c334dcf 100644 --- a/concrete/common/debugging/printing.py +++ b/concrete/common/debugging/printing.py @@ -1,6 +1,6 @@ """functions to print the different graphs we can generate in the package, eg to debug.""" -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import networkx as nx @@ -48,15 +48,16 @@ def shorten_a_constant(constant_data: str): def get_printable_graph( opgraph: OPGraph, show_data_types: bool = False, - highlighted_nodes: Optional[Dict[IntermediateNode, str]] = None, + highlighted_nodes: Optional[Dict[IntermediateNode, List[str]]] = None, ) -> str: """Return a string representing a graph. Args: opgraph (OPGraph): The graph that we want to draw - show_data_types (bool): Whether or not showing data_types of nodes, eg to see their width - highlighted_nodes (Optional[Dict[IntermediateNode, str]]): - The dict of nodes which will be highlighted and their corresponding messages + show_data_types (bool, optional): Whether or not showing data_types of nodes, eg to see + their width. Defaults to False. + highlighted_nodes (Optional[Dict[IntermediateNode, List[str]]], optional): The dict of nodes + which will be highlighted and their corresponding messages. Defaults to None. Returns: str: a string to print or save in a file @@ -145,8 +146,9 @@ def get_printable_graph( returned_str += f"{new_line}\n" if node in highlighted_nodes: - message = highlighted_nodes[node] - returned_str += f"{'^' * len(new_line)} {message}\n" + new_line_len = len(new_line) + message = f"\n{' ' * new_line_len} ".join(highlighted_nodes[node]) + returned_str += f"{'^' * new_line_len} {message}\n" map_table[node] = i i += 1 diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index b46d9e355..39405af8e 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -1,5 +1,5 @@ """Utilities for MLIR conversion.""" -from typing import Dict, Optional, cast +from typing import Dict, List, Optional, cast from ..data_types import Integer from ..data_types.dtypes_helpers import ( @@ -99,7 +99,7 @@ def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) def check_graph_values_compatibility_with_mlir( op_graph: OPGraph, -) -> Optional[Dict[IntermediateNode, str]]: +) -> Optional[Dict[IntermediateNode, List[str]]]: """Make sure the graph outputs are unsigned integers, which is what the compiler supports. Args: @@ -115,7 +115,7 @@ def check_graph_values_compatibility_with_mlir( for node in op_graph.graph.nodes: is_output = node in op_graph.output_nodes.values() if (reason := check_node_compatibility_with_mlir(node, is_output)) is not None: - offending_nodes[node] = reason + offending_nodes[node] = [reason] return None if len(offending_nodes) == 0 else offending_nodes @@ -162,9 +162,9 @@ def update_bit_width_for_mlir(op_graph: OPGraph): # Check that current_node_out_bit_width is supported by the compiler if current_node_out_bit_width > ACCEPTABLE_MAXIMAL_BITWIDTH_FROM_CONCRETE_LIB: - offending_nodes[ - node - ] = f"{current_node_out_bit_width} bits is not supported for the time being" + offending_nodes[node] = [ + f"{current_node_out_bit_width} bits is not supported for the time being" + ] if len(offending_nodes) != 0: raise RuntimeError( diff --git a/tests/common/debugging/test_printing.py b/tests/common/debugging/test_printing.py index c191d7052..622ef67fd 100644 --- a/tests/common/debugging/test_printing.py +++ b/tests/common/debugging/test_printing.py @@ -19,7 +19,7 @@ def test_get_printable_graph_with_offending_nodes(default_compilation_configurat default_compilation_configuration, ) - highlighted_nodes = {opgraph.input_nodes[0]: "foo"} + highlighted_nodes = {opgraph.input_nodes[0]: ["foo"]} without_types = get_printable_graph( opgraph, show_data_types=False, highlighted_nodes=highlighted_nodes @@ -54,7 +54,7 @@ return(%2) """.strip() ) - highlighted_nodes = {opgraph.input_nodes[0]: "foo", opgraph.output_nodes[0]: "bar"} + highlighted_nodes = {opgraph.input_nodes[0]: ["foo"], opgraph.output_nodes[0]: ["bar", "baz"]} without_types = get_printable_graph( opgraph, show_data_types=False, highlighted_nodes=highlighted_nodes @@ -72,6 +72,7 @@ return(%2) %1 = Constant(42) %2 = Add(%0, %1) ^^^^^^^^^^^^^^^^ bar + baz return(%2) """.strip() @@ -86,6 +87,7 @@ return(%2) %1 = Constant(42) # ClearScalar> %2 = Add(%0, %1) # EncryptedScalar> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ bar + baz return(%2) """.strip() From 5a54f2e053dc0deda9f9ec456b2a4feb21ff683b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 27 Oct 2021 09:48:32 +0200 Subject: [PATCH 0469/1104] chore(ci): fix pylint configuration for protected accesses - the check was disabled by default in python special functions closes #752 --- pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index c85b6eb78..507be07ab 100644 --- a/pylintrc +++ b/pylintrc @@ -519,7 +519,7 @@ redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io [CLASSES] # Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no +check-protected-access-in-special-methods=yes # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, From 118e6454b79b3ef923f03d887d5987cba55cad1e Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 26 Oct 2021 13:03:34 +0300 Subject: [PATCH 0470/1104] test(tracing): fix shape mismatch in one of the dot tracing tests --- tests/numpy/test_tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index 3006f5d89..e105c05d6 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -543,7 +543,7 @@ def test_trace_numpy_ufuncs_no_kwargs_no_extra_args(): pytest.param( lambda x, y: numpy.dot(x, y), { - "x": EncryptedTensor(Float(64), shape=(42,)), + "x": EncryptedTensor(Float(64), shape=(10,)), "y": EncryptedTensor(Float(64), shape=(10,)), }, ir.Dot, From eedbe0606b8c46edf888e293d61f1e8390d5d64d Mon Sep 17 00:00:00 2001 From: Umut Date: Tue, 26 Oct 2021 13:14:02 +0300 Subject: [PATCH 0471/1104] feat(tracing): implement tracing of matmul --- concrete/common/debugging/drawing.py | 2 + concrete/common/mlir/utils.py | 3 ++ .../common/representation/intermediate.py | 48 ++++++++++++++++++- concrete/numpy/np_dtypes_helpers.py | 45 +++++++++++++++-- concrete/numpy/tracing.py | 44 ++++++++++------- .../representation/test_intermediate.py | 13 ++++- tests/conftest.py | 7 +++ tests/numpy/test_compile.py | 30 +++++++++++- 8 files changed, 170 insertions(+), 22 deletions(-) diff --git a/concrete/common/debugging/drawing.py b/concrete/common/debugging/drawing.py index c348e5c5d..664c38348 100644 --- a/concrete/common/debugging/drawing.py +++ b/concrete/common/debugging/drawing.py @@ -18,6 +18,7 @@ from ..representation.intermediate import ( Dot, IndexConstant, Input, + MatMul, Mul, Sub, UnivariateFunction, @@ -32,6 +33,7 @@ IR_NODE_COLOR_MAPPING = { UnivariateFunction: "orange", IndexConstant: "black", Dot: "purple", + MatMul: "brown", "UnivariateFunction": "orange", "TLU": "grey", "output": "magenta", diff --git a/concrete/common/mlir/utils.py b/concrete/common/mlir/utils.py index 39405af8e..a40de7557 100644 --- a/concrete/common/mlir/utils.py +++ b/concrete/common/mlir/utils.py @@ -78,6 +78,9 @@ def check_node_compatibility_with_mlir(node: IntermediateNode, is_output: bool) assert_true(len(outputs) == 1) return "indexing is not supported for the time being" + elif isinstance(node, intermediate.MatMul): # constraints for matrix multiplication + return "matrix multiplication is not supported for the time being" + else: # pragma: no cover assert_not_reached("Non IntermediateNode object in the OPGraph") diff --git a/concrete/common/representation/intermediate.py b/concrete/common/representation/intermediate.py index 884801b47..e4493ce74 100644 --- a/concrete/common/representation/intermediate.py +++ b/concrete/common/representation/intermediate.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from collections import deque from copy import deepcopy -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union, cast from loguru import logger @@ -424,3 +424,49 @@ class Dot(IntermediateNode): def label(self) -> str: return "dot" + + +class MatMul(IntermediateNode): + """Return the node representing a matrix multiplication.""" + + _n_in: int = 2 + + def __init__( + self, + inputs: Iterable[BaseValue], + output_dtype: BaseDataType, + ) -> None: + super().__init__(inputs) + assert_true(len(self.inputs) == 2) + + assert_true( + all( + isinstance(input_value, TensorValue) and input_value.ndim == 2 + for input_value in self.inputs + ), + f"MatMul only supports two matrices ({TensorValue.__name__} with ndim == 2)", + ) + + # regular assertions are for mypy to see the inputs are TensorValue + lhs = cast(TensorValue, self.inputs[0]) + rhs = cast(TensorValue, self.inputs[1]) + + assert_true( + lhs.shape[1] == rhs.shape[0], + f"MatMul between matrices of shapes {lhs.shape} and {rhs.shape} " f"is not supported", + ) + + output_shape = (lhs.shape[0], rhs.shape[1]) + output_value = ( + EncryptedTensor(dtype=output_dtype, shape=output_shape) + if (lhs.is_encrypted or rhs.is_encrypted) + else ClearTensor(dtype=output_dtype, shape=output_shape) + ) + + self.outputs = [output_value] + + def evaluate(self, inputs: Dict[int, Any]) -> Any: + return inputs[0] @ inputs[1] + + def label(self) -> str: + return "@" diff --git a/concrete/numpy/np_dtypes_helpers.py b/concrete/numpy/np_dtypes_helpers.py index 78ac6a9bc..9ff9db7af 100644 --- a/concrete/numpy/np_dtypes_helpers.py +++ b/concrete/numpy/np_dtypes_helpers.py @@ -2,7 +2,7 @@ from copy import deepcopy from functools import partial -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List, Tuple, Union import numpy from numpy.typing import DTypeLike @@ -18,6 +18,7 @@ from ..common.data_types.dtypes_helpers import ( from ..common.data_types.floats import Float from ..common.data_types.integers import Integer from ..common.debugging.custom_assert import assert_true +from ..common.tracing import BaseTracer from ..common.values import BaseValue, TensorValue NUMPY_TO_COMMON_DTYPE_MAPPING: Dict[numpy.dtype, BaseDataType] = { @@ -182,9 +183,10 @@ def get_base_value_for_numpy_or_python_constant_data( return constant_data_value -def get_numpy_function_output_dtype( +def get_numpy_function_output_dtype_from_input_dtypes( function: Union[numpy.ufunc, Callable], input_dtypes: List[BaseDataType], + input_shapes: List[Tuple[int, ...]], ) -> List[numpy.dtype]: """Record the output dtype of a numpy function given some input types. @@ -193,6 +195,8 @@ def get_numpy_function_output_dtype( be recorded input_dtypes (List[BaseDataType]): BaseDataTypes in the same order as they will be used with the function inputs + input_shapes (List[Tuple[int, ...]]): Shapes in the same order as they will be used with + the function inputs Returns: List[numpy.dtype]: The ordered numpy dtypes of the function outputs @@ -206,7 +210,12 @@ def get_numpy_function_output_dtype( input_numpy_dtypes = [convert_base_data_type_to_numpy_dtype(dtype) for dtype in input_dtypes] dummy_inputs = tuple( - dtype.type(1000.0 * numpy.random.random_sample()) for dtype in input_numpy_dtypes + ( + dtype.type(10.0 * numpy.random.random_sample()) + if shape == () + else numpy.abs(numpy.random.randn(*shape) * 10.0).astype(dtype) + ) + for dtype, shape in zip(input_numpy_dtypes, input_shapes) ) # We ignore errors as we may call functions with invalid inputs just to get the proper output @@ -220,6 +229,36 @@ def get_numpy_function_output_dtype( return [output.dtype for output in outputs] +def get_numpy_function_output_dtype_from_input_tracers( + func: Union[numpy.ufunc, Callable], + *input_tracers: BaseTracer, +) -> List[BaseDataType]: + """Determine output dtypes for a numpy function. + + This function is responsible for determining the output dtype + of a numpy function after inputs with specific dtypes are passed to it. + + Args: + func (Union[numpy.ufunc, Callable]): function that is being managed + *input_tracers (BaseTracer): inputs to the function + + Returns: + List[numpy.dtype]: appropriate BaseDataType for each output of the function + """ + + input_shapes = [ + input_tracer.output.shape if isinstance(input_tracer.output, TensorValue) else () + for input_tracer in input_tracers + ] + output_dtypes = get_numpy_function_output_dtype_from_input_dtypes( + func, + [input_tracer.output.dtype for input_tracer in input_tracers], + input_shapes, + ) + common_output_dtypes = [convert_numpy_dtype_to_base_data_type(dtype) for dtype in output_dtypes] + return common_output_dtypes + + def get_constructor_for_numpy_or_python_constant_data(constant_data: Any): """Get the constructor for the numpy constant data or python dtype. diff --git a/concrete/numpy/tracing.py b/concrete/numpy/tracing.py index dbb2f25b6..8b5488d20 100644 --- a/concrete/numpy/tracing.py +++ b/concrete/numpy/tracing.py @@ -9,14 +9,14 @@ from numpy.typing import DTypeLike from ..common.data_types.dtypes_helpers import mix_values_determine_holding_dtype from ..common.debugging.custom_assert import assert_true from ..common.operator_graph import OPGraph -from ..common.representation.intermediate import Constant, Dot, UnivariateFunction +from ..common.representation.intermediate import Constant, Dot, MatMul, UnivariateFunction from ..common.tracing import BaseTracer, make_input_tracers, prepare_function_parameters from ..common.values import BaseValue from .np_dtypes_helpers import ( SUPPORTED_NUMPY_DTYPES_CLASS_TYPES, convert_numpy_dtype_to_base_data_type, get_base_value_for_numpy_or_python_constant_data, - get_numpy_function_output_dtype, + get_numpy_function_output_dtype_from_input_tracers, ) from .np_indexing_helpers import process_indexing_element @@ -139,16 +139,6 @@ class NPTracer(BaseTracer): def _make_const_input_tracer(self, constant_data: Any) -> "NPTracer": return self.__class__([], NPConstant(constant_data), 0) - @staticmethod - def _manage_dtypes(ufunc: Union[numpy.ufunc, Callable], *input_tracers: BaseTracer): - output_dtypes = get_numpy_function_output_dtype( - ufunc, [input_tracer.output.dtype for input_tracer in input_tracers] - ) - common_output_dtypes = [ - convert_numpy_dtype_to_base_data_type(dtype) for dtype in output_dtypes - ] - return common_output_dtypes - @classmethod def _unary_operator( cls, unary_operator, unary_operator_string, *input_tracers: "NPTracer", **kwargs @@ -159,7 +149,10 @@ class NPTracer(BaseTracer): NPTracer: The output NPTracer containing the traced function """ assert_true(len(input_tracers) == 1) - common_output_dtypes = cls._manage_dtypes(unary_operator, *input_tracers) + common_output_dtypes = get_numpy_function_output_dtype_from_input_tracers( + unary_operator, + *input_tracers, + ) assert_true(len(common_output_dtypes) == 1) traced_computation = UnivariateFunction( @@ -211,7 +204,10 @@ class NPTracer(BaseTracer): def arbitrary_func(x, baked_constant, **kwargs): return binary_operator(x, baked_constant, **kwargs) - common_output_dtypes = cls._manage_dtypes(binary_operator, *input_tracers) + common_output_dtypes = get_numpy_function_output_dtype_from_input_tracers( + binary_operator, + *input_tracers, + ) assert_true(len(common_output_dtypes) == 1) op_kwargs = deepcopy(kwargs) @@ -249,7 +245,7 @@ class NPTracer(BaseTracer): """ assert_true((num_args := len(args)) == 2, f"dot expects 2 inputs got {num_args}") - common_output_dtypes = self._manage_dtypes(numpy.dot, *args) + common_output_dtypes = get_numpy_function_output_dtype_from_input_tracers(numpy.dot, *args) assert_true(len(common_output_dtypes) == 1) traced_computation = Dot( @@ -273,6 +269,9 @@ class NPTracer(BaseTracer): return BaseTracer.__getitem__(self, item) + def __matmul__(self, other): + return self.__array_ufunc__(numpy.matmul, "__call__", self, other) + # Supported functions are either univariate or bivariate for which one of the two # sources is a constant # @@ -340,7 +339,6 @@ class NPTracer(BaseTracer): numpy.logical_not, numpy.logical_or, numpy.logical_xor, - # numpy.matmul, numpy.maximum, numpy.minimum, numpy.negative, @@ -436,9 +434,23 @@ def _on_numpy_multiply(lhs, rhs): return lhs.__mul__(rhs) +def _on_numpy_matmul(lhs, rhs): + common_output_dtypes = get_numpy_function_output_dtype_from_input_tracers( + numpy.matmul, lhs, rhs + ) + assert_true(len(common_output_dtypes) == 1) + + traced_computation = MatMul( + [lhs.output, rhs.output], + common_output_dtypes[0], + ) + return NPTracer([lhs, rhs], traced_computation, output_idx=0) + + NPTracer.UFUNC_ROUTING[numpy.add] = _on_numpy_add NPTracer.UFUNC_ROUTING[numpy.subtract] = _on_numpy_subtract NPTracer.UFUNC_ROUTING[numpy.multiply] = _on_numpy_multiply +NPTracer.UFUNC_ROUTING[numpy.matmul] = _on_numpy_matmul def trace_numpy_function( diff --git a/tests/common/representation/test_intermediate.py b/tests/common/representation/test_intermediate.py index 0228525e8..00e5905ef 100644 --- a/tests/common/representation/test_intermediate.py +++ b/tests/common/representation/test_intermediate.py @@ -1,5 +1,4 @@ """Test file for intermediate representation""" - from copy import deepcopy import numpy @@ -164,6 +163,18 @@ from concrete.common.values import ClearScalar, ClearTensor, EncryptedScalar, En ), id="IndexConstant, np.array([[1, 2, 3, 4]...[13, 14, 15, 16]])[1:3, 2:0:-1]", ), + pytest.param( + ir.MatMul( + [ + EncryptedTensor(Integer(32, True), shape=(3, 2)), + ClearTensor(Integer(32, True), shape=(2, 3)), + ], + Integer(32, True), + ), + [numpy.arange(1, 7).reshape(3, 2), numpy.arange(1, 7).reshape(2, 3)], + numpy.array([[9, 12, 15], [19, 26, 33], [29, 40, 51]]), + id="MatMul, numpy.arange(1, 7).reshape(3, 2), numpy.arange(1, 7).reshape(2, 3)", + ), ], ) def test_evaluate( diff --git a/tests/conftest.py b/tests/conftest.py index 56cfd08bb..e947978f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ from concrete.common.representation.intermediate import ( IndexConstant, Input, IntermediateNode, + MatMul, Mul, Sub, UnivariateFunction, @@ -167,6 +168,11 @@ def is_equivalent_sub(lhs: Sub, rhs: object) -> bool: return _is_equivalent_to_binary_non_commutative(lhs, rhs) +def is_equivalent_matmul(lhs: MatMul, rhs: object) -> bool: + """Helper function to check if a MatMul node is equivalent to an other object.""" + return isinstance(rhs, MatMul) and is_equivalent_intermediate_node(lhs, rhs) + + def is_equivalent_intermediate_node(lhs: IntermediateNode, rhs: object) -> bool: """Helper function to check if an IntermediateNode node is equivalent to an other object.""" return ( @@ -185,6 +191,7 @@ EQUIVALENT_TEST_FUNC: Dict[Type, Callable[..., bool]] = { Input: is_equivalent_input, Mul: is_equivalent_mul, Sub: is_equivalent_sub, + MatMul: is_equivalent_matmul, } _missing_nodes_in_mapping = ALL_IR_NODES - EQUIVALENT_TEST_FUNC.keys() diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 2be41db05..aa9ffcf87 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -880,6 +880,34 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura "return(%7)\n" ), ), + pytest.param( + lambda x: x @ numpy.ones(shape=(2, 3), dtype=numpy.uint32), + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(3, 2))}, + [(numpy.random.randint(0, 2 ** 3, size=(3, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(3, 2)>\n" # noqa: E501 + "%1 = Constant([[1 1 1] [1 1 1]]) # ClearTensor, shape=(2, 3)>\n" # noqa: E501 + "%2 = MatMul(%0, %1) # EncryptedTensor, shape=(3, 3)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matrix multiplication is not supported for the time being\n" # noqa: E501 + "return(%2)\n" + ), + ), + pytest.param( + lambda x: numpy.matmul(x, numpy.ones(shape=(2, 3), dtype=numpy.uint32)), + {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(3, 2))}, + [(numpy.random.randint(0, 2 ** 3, size=(3, 2)),) for i in range(10)], + ( + "function you are trying to compile isn't supported for MLIR lowering\n" + "\n" + "%0 = x # EncryptedTensor, shape=(3, 2)>\n" # noqa: E501 + "%1 = Constant([[1 1 1] [1 1 1]]) # ClearTensor, shape=(2, 3)>\n" # noqa: E501 + "%2 = MatMul(%0, %1) # EncryptedTensor, shape=(3, 3)>\n" # noqa: E501 + "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matrix multiplication is not supported for the time being\n" # noqa: E501 + "return(%2)\n" + ), + ), ], ) # pylint: enable=line-too-long,unnecessary-lambda @@ -894,7 +922,7 @@ def test_fail_compile(function, parameters, inputset, match, default_compilation default_compilation_configuration, ) - assert str(excinfo.value) == match + assert str(excinfo.value) == match, str(excinfo.value) def test_fail_with_intermediate_signed_values(default_compilation_configuration): From 23d4dead3038ec944e9c13aca858c421961bdecf Mon Sep 17 00:00:00 2001 From: Benoit Chevallier-Mames Date: Wed, 27 Oct 2021 18:08:03 +0200 Subject: [PATCH 0472/1104] chore: remove unnecessary lambda in tests --- Makefile | 5 +++-- tests/numpy/test_compile.py | 8 +++----- tests/numpy/test_debugging.py | 2 -- tests/numpy/test_tracing.py | 14 ++------------ 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 8d592bf42..786cbb83e 100644 --- a/Makefile +++ b/Makefile @@ -46,8 +46,9 @@ pylint_src: .PHONY: pylint_src pylint_tests: - @# Disable duplicate code detection in tests - find ./tests/ -type f -name "*.py" | xargs poetry run pylint --disable=R0801 --rcfile=pylintrc + @# Disable duplicate code detection (R0801) in tests + @# Disable unnecessary lambda (W0108) for tests + find ./tests/ -type f -name "*.py" | xargs poetry run pylint --disable=R0801,W0108 --rcfile=pylintrc .PHONY: pylint_tests pylint_benchmarks: diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index aa9ffcf87..57cc572e1 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -739,7 +739,7 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ) -# pylint: disable=line-too-long,unnecessary-lambda +# pylint: disable=line-too-long @pytest.mark.parametrize( "function,parameters,inputset,match", [ @@ -910,7 +910,7 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura ), ], ) -# pylint: enable=line-too-long,unnecessary-lambda +# pylint: enable=line-too-long def test_fail_compile(function, parameters, inputset, match, default_compilation_configuration): """Test function compile_numpy_function_into_op_graph for a program with signed values""" @@ -993,7 +993,6 @@ def test_small_inputset_treat_warnings_as_errors(): @pytest.mark.parametrize( "function,params,shape,ref_graph_str", [ - # pylint: disable=unnecessary-lambda ( lambda x, y: numpy.dot(x, y), { @@ -1011,7 +1010,6 @@ def test_small_inputset_treat_warnings_as_errors(): "# EncryptedScalar>" "\nreturn(%2)\n", ), - # pylint: enable=unnecessary-lambda ], ) def test_compile_function_with_dot( @@ -1129,7 +1127,7 @@ def test_compile_too_high_bitwidth(default_compilation_configuration): def test_failure_for_signed_output(default_compilation_configuration): """Test that we don't accept signed output""" - function = lambda x: x + (-3) # pylint: disable=unnecessary-lambda # noqa: E731 + function = lambda x: x + (-3) # noqa: E731 input_ranges = ((0, 10),) list_of_arg_names = ["x"] diff --git a/tests/numpy/test_debugging.py b/tests/numpy/test_debugging.py index 267973841..254a7fac2 100644 --- a/tests/numpy/test_debugging.py +++ b/tests/numpy/test_debugging.py @@ -190,7 +190,6 @@ def test_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph_str): @pytest.mark.parametrize( "lambda_f,params,ref_graph_str", [ - # pylint: disable=unnecessary-lambda ( lambda x, y: numpy.dot(x, y), { @@ -199,7 +198,6 @@ def test_print_and_draw_graph_with_direct_tlu(lambda_f, params, ref_graph_str): }, "%0 = x\n%1 = y\n%2 = Dot(%0, %1)\nreturn(%2)\n", ), - # pylint: enable=unnecessary-lambda ], ) def test_print_and_draw_graph_with_dot(lambda_f, params, ref_graph_str): diff --git a/tests/numpy/test_tracing.py b/tests/numpy/test_tracing.py index e105c05d6..441f1ed22 100644 --- a/tests/numpy/test_tracing.py +++ b/tests/numpy/test_tracing.py @@ -394,9 +394,7 @@ def test_tracing_astype( # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint is not happy # with it - # pylint: disable=unnecessary-lambda [lambda x: numpy.invert(x), lambda x: numpy.bitwise_not(x)], - # pylint: enable=unnecessary-lambda ) def test_trace_numpy_fails_for_invert(inputs, function_to_trace): """Check we catch calls to numpy.invert and tell user to change their code""" @@ -446,9 +444,9 @@ def test_trace_numpy_supported_unary_ufuncs(inputs, expected_output_node, functi # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint and flake8 are not happy # with it - # pylint: disable=unnecessary-lambda,cell-var-from-loop + # pylint: disable=cell-var-from-loop function_to_trace = lambda x: function_to_trace_def(x) # noqa: E731 - # pylint: enable=unnecessary-lambda,cell-var-from-loop + # pylint: enable=cell-var-from-loop op_graph = tracing.trace_numpy_function(function_to_trace, inputs) @@ -483,9 +481,7 @@ def test_trace_numpy_ufuncs_not_supported(): # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint and flake8 are not happy # with it - # pylint: disable=unnecessary-lambda function_to_trace = lambda x: numpy.add.reduce(x) # noqa: E731 - # pylint: enable=unnecessary-lambda with pytest.raises(NotImplementedError) as excinfo: tracing.trace_numpy_function(function_to_trace, inputs) @@ -504,9 +500,7 @@ def test_trace_numpy_ufuncs_no_kwargs_no_extra_args(): # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint and flake8 are not happy # with it - # pylint: disable=unnecessary-lambda function_to_trace = lambda x, y, z: numpy.add(x, y, z) # noqa: E731 - # pylint: enable=unnecessary-lambda with pytest.raises(AssertionError) as excinfo: tracing.trace_numpy_function(function_to_trace, inputs) @@ -517,9 +511,7 @@ def test_trace_numpy_ufuncs_no_kwargs_no_extra_args(): # We really need a lambda (because numpy functions are not playing # nice with inspect.signature), but pylint and flake8 are not happy # with it - # pylint: disable=unnecessary-lambda function_to_trace = lambda x, y, z: numpy.add(x, y, out=z) # noqa: E731 - # pylint: enable=unnecessary-lambda with pytest.raises(AssertionError) as excinfo: tracing.trace_numpy_function(function_to_trace, inputs) @@ -530,7 +522,6 @@ def test_trace_numpy_ufuncs_no_kwargs_no_extra_args(): @pytest.mark.parametrize( "function_to_trace,inputs,expected_output_node,expected_output_value", [ - # pylint: disable=unnecessary-lambda pytest.param( lambda x, y: numpy.dot(x, y), { @@ -566,7 +557,6 @@ def test_trace_numpy_ufuncs_no_kwargs_no_extra_args(): ir.Dot, EncryptedScalar(Integer(64, True)), ), - # pylint: enable=unnecessary-lambda ], ) def test_trace_numpy_dot(function_to_trace, inputs, expected_output_node, expected_output_value): From ce17767288a1d59f67e8c74f637e4a84ff4d2b70 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 27 Oct 2021 14:32:53 +0200 Subject: [PATCH 0473/1104] test: use triple quoted strings for long message matches closes #754 --- tests/numpy/test_compile.py | 213 +++++++++++++++++++----------------- 1 file changed, 114 insertions(+), 99 deletions(-) diff --git a/tests/numpy/test_compile.py b/tests/numpy/test_compile.py index 57cc572e1..a9169dbad 100644 --- a/tests/numpy/test_compile.py +++ b/tests/numpy/test_compile.py @@ -748,13 +748,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedScalar(Integer(3, is_signed=False))}, [(i,) for i in range(8)], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = Constant(1) # ClearScalar>\n" # noqa: E501 - "%1 = x # EncryptedScalar>\n" # noqa: E501 - "%2 = Sub(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported\n" # noqa: E501 - "return(%2)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = Constant(1) # ClearScalar> +%1 = x # EncryptedScalar> +%2 = Sub(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported +return(%2) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -762,13 +764,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, [(numpy.random.randint(0, 8, size=(2, 2)),) for i in range(10)], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 - "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 - "return(%2)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = x # EncryptedTensor, shape=(2, 2)> +%1 = Constant(1) # ClearScalar> +%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported +return(%2) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -776,13 +780,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, [(numpy.random.randint(0, 2 ** 3, size=(2, 2)),) for i in range(10)], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 - "%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported\n" # noqa: E501 - "return(%2)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = x # EncryptedTensor, shape=(2, 2)> +%1 = Constant(1) # ClearScalar> +%2 = Add(%0, %1) # EncryptedTensor, shape=(2, 2)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar addition is supported +return(%2) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -790,13 +796,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, [(numpy.random.randint(0, 2 ** 3, size=(2, 2)),) for i in range(10)], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "%1 = Constant(1) # ClearScalar>\n" # noqa: E501 - "%2 = Mul(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar multiplication is supported\n" # noqa: E501 - "return(%2)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = x # EncryptedTensor, shape=(2, 2)> +%1 = Constant(1) # ClearScalar> +%2 = Mul(%0, %1) # EncryptedTensor, shape=(2, 2)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar multiplication is supported +return(%2) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -804,13 +812,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedTensor(Integer(3, is_signed=False), shape=(2, 2))}, [(numpy.random.randint(0, 2 ** 3, size=(2, 2)),) for i in range(10)], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = Constant(127) # ClearScalar>\n" # noqa: E501 - "%1 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "%2 = Sub(%0, %1) # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar subtraction is supported\n" # noqa: E501 - "return(%2)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = Constant(127) # ClearScalar> +%1 = x # EncryptedTensor, shape=(2, 2)> +%2 = Sub(%0, %1) # EncryptedTensor, shape=(2, 2)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar subtraction is supported +return(%2) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -832,15 +842,17 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura (numpy.array([-2]), numpy.array([1])), ], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = x # EncryptedTensor, shape=(1,)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 - "%1 = y # EncryptedTensor, shape=(1,)>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 - "%2 = Dot(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer dot product is supported\n" # noqa: E501 - "return(%2)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = x # EncryptedTensor, shape=(1,)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported +%1 = y # EncryptedTensor, shape=(1,)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported +%2 = Dot(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer dot product is supported +return(%2) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -848,13 +860,15 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedTensor(Integer(3, is_signed=True), shape=(2, 2))}, [(numpy.random.randint(-4, 2 ** 2, size=(2, 2)),) for i in range(10)], ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = x # EncryptedTensor, shape=(2, 2)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported\n" # noqa: E501 # pylint: disable=line-too-long - "%1 = IndexConstant(%0[0]) # EncryptedTensor, shape=(2,)>\n" # noqa: E501 # pylint: disable=line-too-long - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ indexing is not supported for the time being\n" # noqa: E501 # pylint: disable=line-too-long - "return(%1)\n" + """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = x # EncryptedTensor, shape=(2, 2)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported +%1 = IndexConstant(%0[0]) # EncryptedTensor, shape=(2,)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ indexing is not supported for the time being +return(%1) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -862,22 +876,24 @@ def test_compile_function_with_direct_tlu_overflow(default_compilation_configura {"x": EncryptedScalar(Integer(2, False)), "y": EncryptedScalar(Integer(2, False))}, [(i, i) for i in range(10)], ( - "function you are trying to compile isn't supported for MLIR lowering\n\n" - "%0 = x # EncryptedScalar>\n" # noqa: E501 - "%1 = Constant(2.8) # ClearScalar>\n" - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported\n" # noqa: E501 - "%2 = y # EncryptedScalar>\n" # noqa: E501 - "%3 = Constant(9.3) # ClearScalar>\n" - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported\n" # noqa: E501 - "%4 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported\n" # noqa: E501 - "%5 = Add(%2, %3) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported\n" # noqa: E501 - "%6 = Add(%4, %5) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported\n" # noqa: E501 - "%7 = astype(int32)(%6) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 - "return(%7)\n" + """ +function you are trying to compile isn't supported for MLIR lowering\n +%0 = x # EncryptedScalar> +%1 = Constant(2.8) # ClearScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported +%2 = y # EncryptedScalar> +%3 = Constant(9.3) # ClearScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported +%4 = Add(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported +%5 = Add(%2, %3) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported +%6 = Add(%4, %5) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer intermediates are supported +%7 = astype(int32)(%6) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported +return(%7) +""".lstrip() # noqa: E501 ), ), pytest.param( @@ -946,22 +962,20 @@ def test_fail_with_intermediate_signed_values(default_compilation_configuration) show_mlir=True, ) except RuntimeError as error: - # pylint: disable=line-too-long - match = ( - "function you are trying to compile isn't supported for MLIR lowering\n" - "\n" - "%0 = y # EncryptedScalar>\n" # noqa: E501 - "%1 = Constant(10) # ClearScalar>\n" # noqa: E501 - "%2 = x # EncryptedScalar>\n" # noqa: E501 - "%3 = np.negative(%2) # EncryptedScalar>\n" # noqa: E501 - "%4 = Mul(%3, %1) # EncryptedScalar>\n" # noqa: E501 - "%5 = np.absolute(%4) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported\n" # noqa: E501 - "%6 = astype(int32)(%5) # EncryptedScalar>\n" # noqa: E501 - "%7 = Add(%6, %0) # EncryptedScalar>\n" # noqa: E501 - "return(%7)\n" - ) - # pylint: enable=line-too-long + match = """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = y # EncryptedScalar> +%1 = Constant(10) # ClearScalar> +%2 = x # EncryptedScalar> +%3 = np.negative(%2) # EncryptedScalar> +%4 = Mul(%3, %1) # EncryptedScalar> +%5 = np.absolute(%4) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer scalar lookup tables are supported +%6 = astype(int32)(%5) # EncryptedScalar> +%7 = Add(%6, %0) # EncryptedScalar> +return(%7) +""".lstrip() # noqa: E501 # pylint: disable=line-too-long assert str(error) == match raise @@ -1102,17 +1116,17 @@ def test_compile_too_high_bitwidth(default_compilation_configuration): default_compilation_configuration, ) - # pylint: disable=line-too-long assert ( str(excinfo.value) - == "max_bit_width of some nodes is too high for the current version of the compiler (maximum must be 7) which is not compatible with:\n" # noqa: E501 - "%0 = x # EncryptedScalar>\n" # noqa: E501 - "%1 = y # EncryptedScalar>\n" # noqa: E501 - "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 bits is not supported for the time being\n" # noqa: E501 - "return(%2)\n" + == """ +max_bit_width of some nodes is too high for the current version of the compiler (maximum must be 7) which is not compatible with: +%0 = x # EncryptedScalar> +%1 = y # EncryptedScalar> +%2 = Add(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 bits is not supported for the time being +return(%2) +""".lstrip() # noqa: E501 # pylint: disable=line-too-long ) - # pylint: enable=line-too-long # Just ok input_ranges = [(0, 99), (0, 28)] @@ -1147,17 +1161,18 @@ def test_failure_for_signed_output(default_compilation_configuration): default_compilation_configuration, ) - # pylint: disable=line-too-long assert ( str(excinfo.value) - == "function you are trying to compile isn't supported for MLIR lowering\n\n" # noqa: E501 - "%0 = x # EncryptedScalar>\n" # noqa: E501 - "%1 = Constant(-3) # ClearScalar>\n" # noqa: E501 - "%2 = Add(%0, %1) # EncryptedScalar>\n" # noqa: E501 - "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported\n" # noqa: E501 - "return(%2)\n" + == """ +function you are trying to compile isn't supported for MLIR lowering + +%0 = x # EncryptedScalar> +%1 = Constant(-3) # ClearScalar> +%2 = Add(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only scalar unsigned integer outputs are supported +return(%2) +""".lstrip() # noqa: E501 # pylint: disable=line-too-long ) - # pylint: enable=line-too-long def test_compile_with_random_inputset(default_compilation_configuration): From 86b6137fcb35ff10c772a822e30896f57cc1273a Mon Sep 17 00:00:00 2001 From: aquint-zama Date: Wed, 27 Oct 2021 16:08:41 +0200 Subject: [PATCH 0474/1104] docs: update docs template to v0.6.3 close #757 --- docs/_static/css/zama.css | 6 ++++++ docs/_templates/layout.html | 2 +- docs/_templates/versioning.html | 3 +-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/_static/css/zama.css b/docs/_static/css/zama.css index b9dbbc6f2..d2aaf1930 100644 --- a/docs/_static/css/zama.css +++ b/docs/_static/css/zama.css @@ -134,6 +134,12 @@ input[type=color], input[type=date], input[type=datetime-local], input[type=date .rst-versions { font-family: var(--primary-font); } + +/* fix version link color */ +.rst-versions a { + color: var(--primary-color); +} + /* fix version fa-caret-down empty space on click */ .rst-versions.shift-up { overflow-y: unset; diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index cb864e715..40e7dd702 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -16,7 +16,7 @@ - + diff --git a/docs/_templates/versioning.html b/docs/_templates/versioning.html index 8486df5b7..65838d55d 100644 --- a/docs/_templates/versioning.html +++ b/docs/_templates/versioning.html @@ -2,7 +2,6 @@
{{ project }} - {{ version_name }} - + {{ version_name }}
From 212dc363828facf38f57803a20db1b15b1386cc0 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 27 Oct 2021 10:55:58 +0200 Subject: [PATCH 0475/1104] feat: emit loguru warning with reason for subgraph not fusing - catches cases with more than one variable input - catches cases where the shapes are not the same in intermediate nodes refs #645 --- concrete/common/operator_graph.py | 2 +- concrete/common/optimization/topological.py | 99 ++++++++++++++--- .../common/optimization/test_float_fusing.py | 102 +++++++++++++++--- tests/conftest.py | 10 ++ 4 files changed, 184 insertions(+), 29 deletions(-) diff --git a/concrete/common/operator_graph.py b/concrete/common/operator_graph.py index d7355476b..92ed97157 100644 --- a/concrete/common/operator_graph.py +++ b/concrete/common/operator_graph.py @@ -31,7 +31,6 @@ class OPGraph: input_nodes: Dict[int, Input], output_nodes: Dict[int, IntermediateNode], ) -> None: - assert_true(len(input_nodes) > 0, "Got a graph without input nodes which is not supported") assert_true( all(isinstance(node, Input) for node in input_nodes.values()), "Got input nodes that were not Input, which is not supported", @@ -47,6 +46,7 @@ class OPGraph: self.prune_nodes() def __call__(self, *args) -> Union[Any, Tuple[Any, ...]]: + assert_true(len(self.input_nodes) > 0, "Cannot evaluate a graph with no input nodes") inputs = dict(enumerate(args)) assert_true( diff --git a/concrete/common/optimization/topological.py b/concrete/common/optimization/topological.py index ca6cdb012..a7e5a0708 100644 --- a/concrete/common/optimization/topological.py +++ b/concrete/common/optimization/topological.py @@ -1,13 +1,16 @@ """File holding topological optimization/simplification code.""" import itertools +from collections import defaultdict from copy import deepcopy -from typing import Dict, List, Optional, Set, Tuple, cast +from typing import DefaultDict, Dict, List, Optional, Set, Tuple, cast import networkx as nx +from loguru import logger from ..compilation.artifacts import CompilationArtifacts from ..data_types.floats import Float from ..data_types.integers import Integer +from ..debugging import get_printable_graph from ..debugging.custom_assert import assert_true from ..operator_graph import OPGraph from ..representation.intermediate import Constant, Input, IntermediateNode, UnivariateFunction @@ -112,11 +115,30 @@ def convert_float_subgraph_to_fused_node( output must be plugged as the input to the subgraph. """ - subgraph_can_be_fused = subgraph_has_unique_variable_input( - float_subgraph_start_nodes - ) and subgraph_values_allow_fusing(float_subgraph_start_nodes, subgraph_all_nodes) + node_with_issues_for_fusing: DefaultDict[IntermediateNode, List[str]] = defaultdict(list) + subgraph_can_be_fused = subgraph_has_unique_variable_input( + float_subgraph_start_nodes, terminal_node, node_with_issues_for_fusing + ) + + if subgraph_can_be_fused: + # subgraph_values_allow_fusing can be called iff the subgraph has a unique variable input + subgraph_can_be_fused = subgraph_values_allow_fusing( + float_subgraph_start_nodes, subgraph_all_nodes, node_with_issues_for_fusing + ) + + # This test is separate from the previous one to only handle printing issues once if not subgraph_can_be_fused: + float_subgraph = nx.MultiDiGraph(op_graph.graph.subgraph(subgraph_all_nodes)) + float_subgraph_as_op_graph = OPGraph.from_graph(float_subgraph, [], [terminal_node]) + + printable_graph = get_printable_graph( + float_subgraph_as_op_graph, + show_data_types=True, + highlighted_nodes=node_with_issues_for_fusing, + ) + message = f"The following subgraph is not fusable:\n{printable_graph}" + logger.warning(message) return None # Only one variable input node, find which node feeds its input @@ -258,7 +280,8 @@ def find_float_subgraph_with_unique_terminal_node( def subgraph_values_allow_fusing( float_subgraph_start_nodes: Set[IntermediateNode], subgraph_all_nodes: Set[IntermediateNode], -): + node_with_issues_for_fusing: DefaultDict[IntermediateNode, List[str]], +) -> bool: """Check if a subgraph's values are compatible with fusing. A fused subgraph for example only works on an input tensor if the resulting UnivariateFunction @@ -267,6 +290,8 @@ def subgraph_values_allow_fusing( Args: float_subgraph_start_nodes (Set[IntermediateNode]): The nodes starting the float subgraph. subgraph_all_nodes (Set[IntermediateNode]): All the nodes in the float subgraph. + node_with_issues_for_fusing (DefaultDict[IntermediateNode, List[str]]): Dictionary to fill + with potential nodes issues preventing fusing. Returns: bool: True if all inputs and outputs of the nodes in the subgraph are compatible with fusing @@ -286,10 +311,10 @@ def subgraph_values_allow_fusing( # Some UnivariateFunction nodes have baked constants that need to be taken into account for the # max size computation baked_constants_ir_nodes = [ - baked_constant_base_value + baked_constant_ir_node for node in subgraph_all_nodes if isinstance(node, UnivariateFunction) - if (baked_constant_base_value := node.op_attributes.get("baked_constant_ir_node", None)) + if (baked_constant_ir_node := node.op_attributes.get("baked_constant_ir_node", None)) is not None ] @@ -332,26 +357,72 @@ def subgraph_values_allow_fusing( non_constant_nodes = (node for node in subgraph_all_nodes if not isinstance(node, Constant)) - return all( - all( - isinstance(output, TensorValue) and output.shape == variable_input_node_output_shape + nodes_with_different_output_shapes = { + node: [ + (output_idx, output.shape) + for output_idx, output in enumerate(node.outputs) + if isinstance(output, TensorValue) and output.shape != variable_input_node + ] + for node in non_constant_nodes + if any( + isinstance(output, TensorValue) and output.shape != variable_input_node_output_shape for output in node.outputs ) - for node in non_constant_nodes - ) + } + + for node, node_shape_infos in nodes_with_different_output_shapes.items(): + shape_issue_details = "; ".join( + f"#{output_idx}, {output_shape}" for output_idx, output_shape in node_shape_infos + ) + node_with_issues_for_fusing[node].append( + f"output shapes: {shape_issue_details} are not the same as the subgraph's input: " + f"{variable_input_node_output_shape}" + ) + + all_nodes_have_same_shape_as_input = len(nodes_with_different_output_shapes) == 0 + + if not all_nodes_have_same_shape_as_input: + node_with_issues_for_fusing[variable_input_node].append( + f"input node with shape {variable_input_node_output_shape}" + ) + + # All non constant node outputs currently need to have the same shape + return all_nodes_have_same_shape_as_input def subgraph_has_unique_variable_input( float_subgraph_start_nodes: Set[IntermediateNode], + terminal_node: IntermediateNode, + node_with_issues_for_fusing: DefaultDict[IntermediateNode, List[str]], ) -> bool: """Check that only one of the nodes starting the subgraph is variable. Args: float_subgraph_start_nodes (Set[IntermediateNode]): The nodes starting the subgraph. + terminal_node (IntermediateNode): The node ending the float subgraph. + node_with_issues_for_fusing (DefaultDict[IntermediateNode, List[str]]): Dictionary to fill + with potential nodes issues preventing fusing. Returns: bool: True if only one of the nodes is not an Constant """ - # Only one input to the subgraph where computations are done in floats is variable, this + + variable_inputs_list = [ + node for node in float_subgraph_start_nodes if not isinstance(node, Constant) + ] + variable_inputs_num = len(variable_inputs_list) + + # Only one input to the subgraph where computations are done in floats can be variable, this # is the only case we can manage with UnivariateFunction fusing - return sum(not isinstance(node, Constant) for node in float_subgraph_start_nodes) == 1 + has_unique_variable_input = variable_inputs_num == 1 + + if not has_unique_variable_input: + for node in variable_inputs_list: + node_with_issues_for_fusing[node].append( + f"one of {variable_inputs_num} variable inputs (can only have 1 for fusing)" + ) + node_with_issues_for_fusing[terminal_node].append( + f"cannot fuse here as the subgraph has {variable_inputs_num} variable inputs" + ) + + return has_unique_variable_input diff --git a/tests/common/optimization/test_float_fusing.py b/tests/common/optimization/test_float_fusing.py index e9f13c3eb..cbd2ee0ab 100644 --- a/tests/common/optimization/test_float_fusing.py +++ b/tests/common/optimization/test_float_fusing.py @@ -27,6 +27,11 @@ def no_fuse_unhandled(x, y): return intermediate.astype(numpy.int32) +def no_fuse_dot(x): + """No fuse dot""" + return numpy.dot(x, numpy.full((10,), 1.33, dtype=numpy.float64)).astype(numpy.int32) + + def simple_fuse_not_output(x): """Simple fuse not output""" intermediate = x.astype(numpy.float64) @@ -107,34 +112,96 @@ def mix_x_and_y_into_integer_and_call_f(function, x, y): ) +def get_func_params_scalar_int32(func): + """Returns a dict with parameters as scalar int32""" + + return { + param_name: EncryptedScalar(Integer(32, True)) + for param_name in signature(func).parameters.keys() + } + + @pytest.mark.parametrize( - "function_to_trace,fused", + "function_to_trace,fused,params,warning_message", [ - pytest.param(no_fuse, False, id="no_fuse"), - pytest.param(no_fuse_unhandled, False, id="no_fuse_unhandled"), - pytest.param(simple_fuse_not_output, True, id="no_fuse"), - pytest.param(simple_fuse_output, True, id="no_fuse"), + pytest.param(no_fuse, False, get_func_params_scalar_int32(no_fuse), "", id="no_fuse"), + pytest.param( + no_fuse_unhandled, + False, + get_func_params_scalar_int32(no_fuse_unhandled), + """The following subgraph is not fusable: +%0 = x # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one of 2 variable inputs (can only have 1 for fusing) +%1 = Constant(0.7) # ClearScalar> +%2 = y # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one of 2 variable inputs (can only have 1 for fusing) +%3 = Constant(1.3) # ClearScalar> +%4 = Add(%0, %1) # EncryptedScalar> +%5 = Add(%2, %3) # EncryptedScalar> +%6 = Add(%4, %5) # EncryptedScalar> +%7 = astype(int32)(%6) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot fuse here as the subgraph has 2 variable inputs +return(%7)""", # noqa: E501 # pylint: disable=line-too-long + id="no_fuse_unhandled", + ), + pytest.param( + no_fuse_dot, + False, + {"x": EncryptedTensor(Integer(32, True), (10,))}, + """The following subgraph is not fusable: +%0 = x # EncryptedTensor, shape=(10,)> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ input node with shape (10,) +%1 = Constant([1.33 1.33 ... 1.33 1.33]) # ClearTensor, shape=(10,)> +%2 = Dot(%0, %1) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ output shapes: #0, () are not the same as the subgraph's input: (10,) +%3 = astype(int32)(%2) # EncryptedScalar> +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ output shapes: #0, () are not the same as the subgraph's input: (10,) +return(%3)""", # noqa: E501 # pylint: disable=line-too-long + id="no_fuse_dot", + ), + pytest.param( + simple_fuse_not_output, + True, + get_func_params_scalar_int32(simple_fuse_not_output), + None, + id="simple_fuse_not_output", + ), + pytest.param( + simple_fuse_output, + True, + get_func_params_scalar_int32(simple_fuse_output), + None, + id="simple_fuse_output", + ), pytest.param( lambda x, y: mix_x_and_y_intricately_and_call_f(numpy.rint, x, y), True, + get_func_params_scalar_int32(lambda x, y: None), + None, id="mix_x_and_y_intricately_and_call_f_with_rint", ), pytest.param( lambda x, y: mix_x_and_y_and_call_f(numpy.rint, x, y), True, + get_func_params_scalar_int32(lambda x, y: None), + None, id="mix_x_and_y_and_call_f_with_rint", ), ], ) -@pytest.mark.parametrize("input_", [0, 2, 42, 44]) -def test_fuse_float_operations(function_to_trace, fused, input_): +def test_fuse_float_operations( + function_to_trace, + fused, + params, + warning_message, + capfd, + remove_color_codes, +): """Test function for fuse_float_operations""" - params_names = signature(function_to_trace).parameters.keys() - op_graph = trace_numpy_function( function_to_trace, - {param_name: EncryptedScalar(Integer(32, True)) for param_name in params_names}, + params, ) orig_num_nodes = len(op_graph.graph) fuse_float_operations(op_graph) @@ -144,12 +211,19 @@ def test_fuse_float_operations(function_to_trace, fused, input_): assert fused_num_nodes < orig_num_nodes else: assert fused_num_nodes == orig_num_nodes + captured = capfd.readouterr() + assert warning_message in remove_color_codes(captured.err) - input_ = numpy.int32(input_) + for input_ in [0, 2, 42, 44]: + inputs = () + for param_input_value in params.values(): + if param_input_value.is_scalar: + input_ = numpy.int32(input_) + else: + input_ = numpy.full(param_input_value.shape, input_, dtype=numpy.int32) + inputs += (input_,) - num_params = len(params_names) - inputs = (input_,) * num_params - assert function_to_trace(*inputs) == op_graph(*inputs) + assert numpy.array_equal(function_to_trace(*inputs), op_graph(*inputs)) def subtest_tensor_no_fuse(fun, tensor_shape): diff --git a/tests/conftest.py b/tests/conftest.py index e947978f6..fda1dc7bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ """PyTest configuration file""" import json import operator +import re from pathlib import Path from typing import Callable, Dict, Type @@ -264,3 +265,12 @@ def default_compilation_configuration(): dump_artifacts_on_unexpected_failures=False, treat_warnings_as_errors=True, ) + + +REMOVE_COLOR_CODES_RE = re.compile(r"\x1b[^m]*m") + + +@pytest.fixture +def remove_color_codes(): + """Return the re object to remove color codes""" + return lambda x: REMOVE_COLOR_CODES_RE.sub("", x) From 2fa3a8bcbee8f0414ec119520d47378dddcb2861 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 28 Oct 2021 12:09:14 +0200 Subject: [PATCH 0476/1104] chore(ci): use aws command line and credentials setup for aws tasks --- .github/workflows/continuous-integration.yaml | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index b13e50e50..da7775d58 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -353,30 +353,32 @@ jobs: uses: actions/download-artifact@3be87be14a055c47b01d3bd88f8fe02320a9bb60 with: name: html-docs + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} - name: Publish Documentation to S3 id: publish if: ${{ steps.download.outcome == 'success' && !cancelled() }} - uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 - with: - args: --delete --acl public-read env: AWS_S3_BUCKET: ${{ steps.docs-push-infos.outputs.aws-bucket }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} SOURCE_DIR: '.' DEST_DIR: ${{ steps.docs-push-infos.outputs.dest-dir }} + run: | + aws s3 sync "${SOURCE_DIR}" s3://"${AWS_S3_BUCKET}/${DEST_DIR}" --delete --acl public-read - name: Invalidate CloudFront Cache if: ${{ steps.publish.outcome == 'success' }} - uses: awact/cloudfront-action@8bcfabc7b4bbc0cb8e55e48527f0e3a6d681627c env: SOURCE_PATH: "/${{ steps.docs-push-infos.outputs.dest-dir }}/*" - AWS_REGION: ${{ secrets.AWS_REGION }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} DISTRIBUTION_ID: ${{ steps.docs-push-infos.outputs.aws-distribution }} + run: | + aws cloudfront create-invalidation \ + --distribution-id "${DISTRIBUTION_ID}" \ + --paths "${SOURCE_PATH}" - name: Set notification report id: report @@ -620,39 +622,38 @@ jobs: if: ${{ success() && !cancelled() }} run: | docker image push --all-tags "${RELEASE_IMAGE_BASE}" + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} - name: Push release documentation if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) }} - uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 - with: - args: --delete --acl public-read env: AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} SOURCE_DIR: ${{ steps.download-docs.outputs.download-path }} DEST_DIR: 'concretefhe/${{ env.PROJECT_VERSION }}' + run: | + aws s3 sync "${SOURCE_DIR}" s3://"${AWS_S3_BUCKET}/${DEST_DIR}" --delete --acl public-read + - name: Push release documentation as stable if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} - uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 - with: - args: --delete --acl public-read env: AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} SOURCE_DIR: ${{ steps.download-docs.outputs.download-path }} DEST_DIR: 'concretefhe/stable' + run: | + aws s3 sync "${SOURCE_DIR}" s3://"${AWS_S3_BUCKET}/${DEST_DIR}" --delete --acl public-read - name: Invalidate CloudFront Cache for stable if: ${{ success() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} - uses: awact/cloudfront-action@8bcfabc7b4bbc0cb8e55e48527f0e3a6d681627c env: SOURCE_PATH: "/concretefhe/stable/*" - AWS_REGION: ${{ secrets.AWS_REGION }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} DISTRIBUTION_ID: ${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }} + run: | + aws cloudfront create-invalidation \ + --distribution-id "${DISTRIBUTION_ID}" \ + --paths "${SOURCE_PATH}" - name: Create GitHub release if: ${{ success() && !cancelled() }} id: create-release From d93015836ee7adb3b230d05b5aeeb684a2d9d71b Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Fri, 29 Oct 2021 11:55:33 +0200 Subject: [PATCH 0477/1104] chore(ci): give the possibility to version cache and clear it - will help resolve issues with poetry cache --- .github/workflows/continuous-integration.yaml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index da7775d58..48d3d9a43 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -178,12 +178,13 @@ jobs: ~/.cache/pip ~/.cache/pypoetry # Ignore line break in the evaluated double quoted string - key: "${{ runner.os }}-build-${{ matrix.python-version }}-\ + key: "${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-${{ matrix.python-version }}-\ ${{ hashFiles('poetry.lock') }}" restore-keys: | - ${{ runner.os }}-build-${{ matrix.python-version }}- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-${{ matrix.python-version }}- + ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build- + ${{ secrets.CACHE_VERSION }}-${{ runner.os }}- + ${{ secrets.CACHE_VERSION }}- - name: Install dependencies id: install-deps run: | @@ -490,11 +491,13 @@ jobs: ~/.cache/pip ~/.cache/pypoetry # Use python 3.8 as it is the version available in ubuntu 20.04 and we develop with it - key: "${{ runner.os }}-build-3.8-${{ hashFiles('poetry.lock') }}" + key: "$${{ secrets.CACHE_VERSION }}-{{ runner.os }}-build-3.8-\ + ${{ hashFiles('poetry.lock') }}" restore-keys: | - ${{ runner.os }}-build-3.8- - ${{ runner.os }}-build- - ${{ runner.os }}- + ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build-3.8- + ${{ secrets.CACHE_VERSION }}-${{ runner.os }}-build- + ${{ secrets.CACHE_VERSION }}-${{ runner.os }}- + ${{ secrets.CACHE_VERSION }}- # See #570 To be updated to only install required dependencies group with poetry 1.2 and # remove graphviz installs which are only required for the actual package and not dev tools - name: Install dependencies From d749f80b8edf524300cc6fc9c617284a867cc632 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Wed, 27 Oct 2021 15:53:22 +0200 Subject: [PATCH 0478/1104] chore: add beautifulsoup4 as dev dependency to manipulate versions.html --- poetry.lock | 63 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index d755bc035..8011c12f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -84,6 +84,21 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "beautifulsoup4" +version = "4.10.0" +description = "Screen-scraping library" +category = "dev" +optional = false +python-versions = ">3.0.0" + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "black" version = "21.9b0" @@ -1646,6 +1661,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "soupsieve" +version = "2.2.1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "sphinx" version = "4.2.0" @@ -1965,7 +1988,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.9" -content-hash = "779f0b068ef721247370db40ff067c6207e70a922fa6e66958f30ede6a86c502" +content-hash = "9dc0f5741bf4782a35eb719e3a798ba7226445020e391bc1ec3be7fd7980ab72" [metadata.files] alabaster = [ @@ -2009,6 +2032,10 @@ backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, + {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, +] black = [ {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, @@ -2393,12 +2420,22 @@ markdown-it-py = [ {file = "markdown_it_py-1.1.0-py3-none-any.whl", hash = "sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2407,14 +2444,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2424,6 +2468,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2878,24 +2925,32 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"}, {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"}, {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"}, {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"}, {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, @@ -2903,6 +2958,8 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"}, {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, @@ -3012,6 +3069,10 @@ snowballstemmer = [ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] +soupsieve = [ + {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, + {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, +] sphinx = [ {file = "Sphinx-4.2.0-py3-none-any.whl", hash = "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"}, {file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"}, diff --git a/pyproject.toml b/pyproject.toml index 8f8ae9139..bde2de042 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ GitPython = "^3.1.24" pytest-xdist = "^2.4.0" pytest-randomly = "^3.10.1" pygments-style-tomorrow = "^1.0.0" +beautifulsoup4 = "^4.10.0" [build-system] requires = ["poetry-core>=1.0.0"] From eb54cec065f0a6e710ab99b57077ef893568b770 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Thu, 28 Oct 2021 14:30:43 +0200 Subject: [PATCH 0479/1104] docs(ci): manage versions.html - create generate_versions_html.py - update workflow to be able to push pre-releases on preprod server closes #738 --- .github/workflows/continuous-integration.yaml | 69 ++++++-- docs/_templates/versions.html | 28 +++ docs/versions.html | 23 --- .../actions_utils/generate_versions_html.py | 160 ++++++++++++++++++ 4 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 docs/_templates/versions.html delete mode 100644 docs/versions.html create mode 100644 script/actions_utils/generate_versions_html.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 48d3d9a43..d3049b75a 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -598,6 +598,49 @@ jobs: with: name: changelog path: ${{ env.ARTIFACTS_RAW_DIR }}/changelog/ + - name: Prepare docs push + id: docs-push-infos + run: | + if [[ ${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_BUCKET_NAME }} != "" ]] && \ + [[ ${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_DISTRIBUTION_ID }} != "" ]] && \ + [[ "${IS_PRERELEASE}" == "true" ]]; then + echo "::set-output name=aws-bucket::${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_BUCKET_NAME }}" + echo "::set-output name=aws-distribution::${{ secrets.AWS_REPO_PREPROD_DOCUMENTATION_DISTRIBUTION_ID }}" + else + echo "::set-output name=aws-bucket::${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }}" + echo "::set-output name=aws-distribution::${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }}" + fi + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Update versions.html for docs + if: ${{ success() && !cancelled() }} + run: | + DOWNLOADED_HTML_FILE=$(mktemp --suffix=.html) + OUTPUT_HTML_FILE=$(mktemp --suffix=.html) + + aws s3api get-object \ + --bucket ${{ steps.docs-push-infos.outputs.aws-bucket }} \ + --key concretefhe/versions.html "${DOWNLOADED_HTML_FILE}" + + python3 ./script/actions_utils/generate_versions_html.py \ + --add-versions "${PROJECT_VERSION}" \ + --versions-html-file "${DOWNLOADED_HTML_FILE}" \ + --output-html "${OUTPUT_HTML_FILE}" + + aws s3 cp "${OUTPUT_HTML_FILE}" \ + s3://${{ steps.docs-push-infos.outputs.aws-bucket }}/concretefhe/versions.html \ + --acl public-read + + aws cloudfront create-invalidation \ + --distribution-id ${{ steps.docs-push-infos.outputs.aws-distribution }} \ + --paths /concretefhe/versions.html + + # Copy to docs to keep a version in docs artifacts + cp "${OUTPUT_HTML_FILE}" "${RAW_DOCS_DIR}"/versions.html - name: Create ready to upload/packaged artifacts and release body if: ${{ success() && !cancelled() }} env: @@ -607,6 +650,8 @@ jobs: pushd "${RAW_DOCS_DIR}" zip -r "${ARTIFACTS_PACKAGED_DIR}/html-docs.zip" ./* tar -cvzf "${ARTIFACTS_PACKAGED_DIR}/html-docs.tar.gz" ./* + # Remove the versions.html to avoid pushing it to S3 but have it in release artifacts + rm versions.html popd cp "${RAW_CHANGELOG_DIR}"/* "${ARTIFACTS_PACKAGED_DIR}" ls -a "${ARTIFACTS_PACKAGED_DIR}" @@ -615,26 +660,20 @@ jobs: echo "RELEASE_BODY_FILE=${RELEASE_BODY_FILE}" >> "$GITHUB_ENV" cp ./script/actions_utils/RELEASE_TEMPLATE.md "${RELEASE_BODY_FILE}" - echo "Docker Image: ${RELEASE_IMG_GIT_TAG}" >> "${RELEASE_BODY_FILE}" - if [[ "${IS_PRERELEASE}" == "false" ]]; then - echo "Documentation: https://docs.zama.ai/concretefhe/${PROJECT_VERSION}" >> "${RELEASE_BODY_FILE}" - fi - echo "" >> "${RELEASE_BODY_FILE}" + { + echo "Docker Image: ${RELEASE_IMG_GIT_TAG}"; + echo "Documentation: https://${{ steps.docs-push-infos.outputs.aws-bucket }}/concretefhe/${PROJECT_VERSION}"; + echo ""; + } >> "${RELEASE_BODY_FILE}" cat "${RAW_CHANGELOG_DIR}"/* >> "${RELEASE_BODY_FILE}" - name: Push release docker image if: ${{ success() && !cancelled() }} run: | docker image push --all-tags "${RELEASE_IMAGE_BASE}" - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@0d9a5be0dceea74e09396820e1e522ba4a110d2f - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} - name: Push release documentation - if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) }} + if: ${{ success() && !cancelled() }} env: - AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} + AWS_S3_BUCKET: ${{ steps.docs-push-infos.outputs.aws-bucket }} SOURCE_DIR: ${{ steps.download-docs.outputs.download-path }} DEST_DIR: 'concretefhe/${{ env.PROJECT_VERSION }}' run: | @@ -643,7 +682,7 @@ jobs: - name: Push release documentation as stable if: ${{ success() && !cancelled() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} env: - AWS_S3_BUCKET: ${{ secrets.AWS_REPO_DOCUMENTATION_BUCKET_NAME }} + AWS_S3_BUCKET: ${{ steps.docs-push-infos.outputs.aws-bucket }} SOURCE_DIR: ${{ steps.download-docs.outputs.download-path }} DEST_DIR: 'concretefhe/stable' run: | @@ -652,7 +691,7 @@ jobs: if: ${{ success() && !fromJSON(env.IS_PRERELEASE) && fromJSON(env.IS_LATEST) }} env: SOURCE_PATH: "/concretefhe/stable/*" - DISTRIBUTION_ID: ${{ secrets.AWS_REPO_DOCUMENTATION_DISTRIBUTION_ID }} + DISTRIBUTION_ID: ${{ steps.docs-push-infos.outputs.aws-distribution }} run: | aws cloudfront create-invalidation \ --distribution-id "${DISTRIBUTION_ID}" \ diff --git a/docs/_templates/versions.html b/docs/_templates/versions.html new file mode 100644 index 000000000..8c221c407 --- /dev/null +++ b/docs/_templates/versions.html @@ -0,0 +1,28 @@ + + + + + + + +
+
+

+ ConcreteFHE Documentation +

+
+

+ + Pick a version + +

+ +
+
+
+ + diff --git a/docs/versions.html b/docs/versions.html deleted file mode 100644 index 7f3b15535..000000000 --- a/docs/versions.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - -
-
-

ConcreteFHE Documentation

-
-

Pick a version

- -
-
-
- - diff --git a/script/actions_utils/generate_versions_html.py b/script/actions_utils/generate_versions_html.py new file mode 100644 index 000000000..6ed0b43c0 --- /dev/null +++ b/script/actions_utils/generate_versions_html.py @@ -0,0 +1,160 @@ +"""Tool to manage the versions.html file at the root of our docs sites.""" + +import argparse +from pathlib import Path + +from bs4 import BeautifulSoup +from bs4.element import Tag +from semver import VersionInfo + +VERSIONS_LIST_ID = "versions-list" + + +def strip_leading_v(version_str: str): + """Strip leading v of a version which is not SemVer compatible.""" + return version_str[1:] if version_str.startswith("v") else version_str + + +def create_list_element(soup: BeautifulSoup, contents: Tag) -> Tag: + """Create a list element for links. + + Args: + soup (BeautifulSoup): The soup to use to create the tag. + + Returns: + Tag: tag containing
  • . + """ + new_list_element = soup.new_tag("li", **{"class": "toctree-l1"}) + new_list_element.contents.append(contents) + return new_list_element + + +def create_link_tag_set_string(soup: BeautifulSoup, version_string: str) -> Tag: + """Create a link tag on the given soup to version specified by version_string. + + Args: + soup (BeautifulSoup): The soup to use to create the tag. + version_string (str): The version string to use. + + Returns: + Tag: tag containing {version_string}. + """ + new_tag = soup.new_tag( + "a", + **{ + "href": f"{version_string}/", + "class": "reference internal", + }, + ) + + new_tag.string = version_string + return new_tag + + +def main(args): + """Entry point.""" + + invalid_versions = [ + version + for version in args.add_versions + if not VersionInfo.isvalid(strip_leading_v(version)) + ] + if len(invalid_versions) > 0: + raise RuntimeError(f"Found invalid versions: {invalid_versions}") + + version_html = None + version_html_file_path = Path(args.versions_html_file).resolve() + with open(version_html_file_path, "r", encoding="utf-8") as f: + version_html = BeautifulSoup(f, "html.parser") + + if version_html is None: + raise RuntimeError(f"An error occured while trying to load {str(version_html_file_path)}") + + print(version_html) + + version_list = version_html.find(id=VERSIONS_LIST_ID) + if version_list is None or version_list.name != "ul": + raise RuntimeError(f"Could not find
    + {#- SIDE NAV, TOGGLES ON MOBILE #} + + +
    + + {#- MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #} + {#- Translators: This is an ARIA section label for the navigation menu that is visible when viewing the page on mobile devices -#} + + +
    + {%- block content %} + {%- if theme_style_external_links|tobool %} + +
    +