From c45b9ffa43d4e6fd1909b41d15aa40a5516ca7a1 Mon Sep 17 00:00:00 2001 From: Umut Date: Mon, 4 Apr 2022 13:32:24 +0200 Subject: [PATCH] feat: rewrite tests --- .github/workflows/continuous-integration.yaml | 2 +- .gitignore | 4 +- Makefile | 2 +- tests/__init__.py | 3 + tests/compilation/__init__.py | 3 + tests/compilation/test_artifacts.py | 61 ++ tests/compilation/test_circuit.py | 131 ++++ tests/compilation/test_compiler.py | 122 ++++ tests/compilation/test_configuration.py | 28 + tests/compilation/test_decorator.py | 86 +++ tests/conftest.py | 298 ++++++++ tests/dtypes/__init__.py | 3 + tests/dtypes/test_float.py | 92 +++ tests/dtypes/test_integer.py | 691 ++++++++++++++++++ tests/dtypes/test_utils.py | 77 ++ tests/execution/__init__.py | 3 + tests/execution/test_add.py | 159 ++++ tests/execution/test_concat.py | 176 +++++ tests/execution/test_convolution.py | 206 ++++++ tests/execution/test_direct_table_lookup.py | 299 ++++++++ tests/execution/test_dot.py | 47 ++ tests/execution/test_matmul.py | 158 ++++ tests/execution/test_mul.py | 76 ++ tests/execution/test_neg.py | 46 ++ tests/execution/test_others.py | 589 +++++++++++++++ tests/execution/test_reshape.py | 170 +++++ tests/execution/test_static_indexing.py | 193 +++++ tests/execution/test_sub.py | 56 ++ tests/execution/test_sum.py | 114 +++ tests/internal/__init__.py | 3 + tests/internal/test_utils.py | 29 + tests/mlir/__init__.py | 3 + tests/mlir/test_graph_converter.py | 337 +++++++++ tests/representation/__init__.py | 3 + tests/representation/test_graph.py | 47 ++ tests/representation/test_node.py | 237 ++++++ tests/representation/test_utils.py | 56 ++ tests/tracing/__init__.py | 3 + tests/tracing/test_tracer.py | 44 ++ tests/values/__init__.py | 3 + tests/values/test_value.py | 321 ++++++++ 41 files changed, 4977 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/compilation/__init__.py create mode 100644 tests/compilation/test_artifacts.py create mode 100644 tests/compilation/test_circuit.py create mode 100644 tests/compilation/test_compiler.py create mode 100644 tests/compilation/test_configuration.py create mode 100644 tests/compilation/test_decorator.py create mode 100644 tests/conftest.py create mode 100644 tests/dtypes/__init__.py create mode 100644 tests/dtypes/test_float.py create mode 100644 tests/dtypes/test_integer.py create mode 100644 tests/dtypes/test_utils.py create mode 100644 tests/execution/__init__.py create mode 100644 tests/execution/test_add.py create mode 100644 tests/execution/test_concat.py create mode 100644 tests/execution/test_convolution.py create mode 100644 tests/execution/test_direct_table_lookup.py create mode 100644 tests/execution/test_dot.py create mode 100644 tests/execution/test_matmul.py create mode 100644 tests/execution/test_mul.py create mode 100644 tests/execution/test_neg.py create mode 100644 tests/execution/test_others.py create mode 100644 tests/execution/test_reshape.py create mode 100644 tests/execution/test_static_indexing.py create mode 100644 tests/execution/test_sub.py create mode 100644 tests/execution/test_sum.py create mode 100644 tests/internal/__init__.py create mode 100644 tests/internal/test_utils.py create mode 100644 tests/mlir/__init__.py create mode 100644 tests/mlir/test_graph_converter.py create mode 100644 tests/representation/__init__.py create mode 100644 tests/representation/test_graph.py create mode 100644 tests/representation/test_node.py create mode 100644 tests/representation/test_utils.py create mode 100644 tests/tracing/__init__.py create mode 100644 tests/tracing/test_tracer.py create mode 100644 tests/values/__init__.py create mode 100644 tests/values/test_value.py diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index 5071fb610..c6a3dc4e4 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -319,7 +319,7 @@ jobs: id: coverage if: ${{ always() && fromJSON(env.IS_REF_BUILD) && steps.pytest.outcome != 'skipped' && !cancelled() }} run: | - ./script/actions_utils/coverage.sh global-coverage-infos.json + ./script/actions_utils/coverage.sh .global-coverage.json - name: Comment with coverage uses: marocchino/sticky-pull-request-comment@39c5b5dc7717447d0cba270cd115037d32d28443 diff --git a/.gitignore b/.gitignore index 888bbd1cc..594bed4a8 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ diff-coverage.txt *.py,cover .hypothesis/ .pytest_cache/ -global-coverage-infos.json +.global-coverage.json # Translations *.mo @@ -133,5 +133,5 @@ dmypy.json # Pyre type checker .pyre/ -# concrete compilation artifacts +# Compilation Artifacts .artifacts diff --git a/Makefile b/Makefile index 75e74e325..ed55e23a0 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ pcc_internal: $(PCC_DEPS) .PHONY: pytest # Run pytest pytest: poetry run pytest -svv \ - --global-coverage-infos-json=global-coverage-infos.json \ + --global-coverage=.global-coverage.json \ -n $$(./script/make_utils/ncpus.sh) \ --cov=$(SRC_DIR) --cov-fail-under=100 \ --randomly-dont-reorganize \ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..03a1d67ad --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of `concrete.numpy` namespace. +""" diff --git a/tests/compilation/__init__.py b/tests/compilation/__init__.py new file mode 100644 index 000000000..e5aca87e2 --- /dev/null +++ b/tests/compilation/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of `concrete.numpy.compilation` namespace. +""" diff --git a/tests/compilation/test_artifacts.py b/tests/compilation/test_artifacts.py new file mode 100644 index 000000000..a875368fa --- /dev/null +++ b/tests/compilation/test_artifacts.py @@ -0,0 +1,61 @@ +""" +Tests of `CompilationArtifacts` class. +""" + +import tempfile +from pathlib import Path + +from concrete.numpy.compilation import CompilationArtifacts, compiler + + +def test_artifacts_export(helpers): + """ + Test `export` method of `Compilation` class. + """ + + with tempfile.TemporaryDirectory() as path: + tmpdir = Path(path) + + configuration = helpers.configuration() + artifacts = CompilationArtifacts(tmpdir) + + @compiler({"x": "encrypted"}, configuration=configuration, artifacts=artifacts) + def f(x): + return x + 10 + + inputset = range(100) + f.compile(inputset) + + artifacts.export() + + assert (tmpdir / "environment.txt").exists() + assert (tmpdir / "requirements.txt").exists() + + assert (tmpdir / "function.txt").exists() + assert (tmpdir / "parameters.txt").exists() + + assert (tmpdir / "1.initial.graph.txt").exists() + assert (tmpdir / "1.initial.graph.png").exists() + + assert (tmpdir / "2.final.graph.txt").exists() + assert (tmpdir / "2.final.graph.png").exists() + + assert (tmpdir / "bounds.txt").exists() + assert (tmpdir / "mlir.txt").exists() + + artifacts.export() + + assert (tmpdir / "environment.txt").exists() + assert (tmpdir / "requirements.txt").exists() + + assert (tmpdir / "function.txt").exists() + assert (tmpdir / "parameters.txt").exists() + + assert (tmpdir / "1.initial.graph.txt").exists() + assert (tmpdir / "1.initial.graph.png").exists() + + assert (tmpdir / "2.final.graph.txt").exists() + assert (tmpdir / "2.final.graph.png").exists() + + assert (tmpdir / "bounds.txt").exists() + assert (tmpdir / "mlir.txt").exists() diff --git a/tests/compilation/test_circuit.py b/tests/compilation/test_circuit.py new file mode 100644 index 000000000..83de20085 --- /dev/null +++ b/tests/compilation/test_circuit.py @@ -0,0 +1,131 @@ +""" +Tests of `Circuit` class. +""" + +import tempfile +from pathlib import Path + +import numpy as np +import pytest + +from concrete.numpy.compilation import compiler + + +def test_circuit_str(helpers): + """ + Test `__str__` method of `Circuit` class. + """ + + configuration = helpers.configuration() + + @compiler({"x": "encrypted", "y": "encrypted"}, configuration=configuration) + def f(x, y): + return x + y + + inputset = [(np.random.randint(0, 2 ** 4), np.random.randint(0, 2 ** 5)) for _ in range(100)] + circuit = f.compile(inputset) + + assert str(circuit) == ( + """ + +%0 = x # EncryptedScalar +%1 = y # EncryptedScalar +%2 = add(%0, %1) # EncryptedScalar +return %2 + + """.strip() + ) + + +def test_circuit_draw(helpers): + """ + Test `draw` method of `Circuit` class. + """ + + configuration = helpers.configuration() + + @compiler({"x": "encrypted", "y": "encrypted"}, configuration=configuration) + def f(x, y): + return x + y + + inputset = [(np.random.randint(0, 2 ** 4), np.random.randint(0, 2 ** 5)) for _ in range(100)] + circuit = f.compile(inputset) + + with tempfile.TemporaryDirectory() as path: + tmpdir = Path(path) + + png = tmpdir / "drawing.png" + circuit.draw(save_to=png) + + assert png.exists() + + +def test_circuit_bad_run(helpers): + """ + Test `run` method of `Circuit` class with bad parameters. + """ + + configuration = helpers.configuration() + + @compiler({"x": "encrypted", "y": "encrypted"}, configuration=configuration) + def f(x, y): + return x + y + + inputset = [(np.random.randint(0, 2 ** 4), np.random.randint(0, 2 ** 5)) for _ in range(100)] + circuit = f.compile(inputset) + + # with 1 argument + # --------------- + + with pytest.raises(ValueError) as excinfo: + circuit.run(1) + + assert str(excinfo.value) == "Expected 2 inputs but got 1" + + # with 3 arguments + # ---------------- + + with pytest.raises(ValueError) as excinfo: + circuit.run(1, 2, 3) + + assert str(excinfo.value) == "Expected 2 inputs but got 3" + + # with negative argument 0 + # ------------------------ + + with pytest.raises(ValueError) as excinfo: + circuit.run(-1, 11) + + assert str(excinfo.value) == ( + "Expected argument 0 to be EncryptedScalar but it's EncryptedScalar" + ) + + # with negative argument 1 + # ------------------------ + + with pytest.raises(ValueError) as excinfo: + circuit.run(1, -11) + + assert str(excinfo.value) == ( + "Expected argument 1 to be EncryptedScalar but it's EncryptedScalar" + ) + + # with large argument 0 + # --------------------- + + with pytest.raises(ValueError) as excinfo: + circuit.run(100, 10) + + assert str(excinfo.value) == ( + "Expected argument 0 to be EncryptedScalar but it's EncryptedScalar" + ) + + # with large argument 1 + # --------------------- + + with pytest.raises(ValueError) as excinfo: + circuit.run(1, 100) + + assert str(excinfo.value) == ( + "Expected argument 1 to be EncryptedScalar but it's EncryptedScalar" + ) diff --git a/tests/compilation/test_compiler.py b/tests/compilation/test_compiler.py new file mode 100644 index 000000000..069793340 --- /dev/null +++ b/tests/compilation/test_compiler.py @@ -0,0 +1,122 @@ +""" +Tests of `Compiler` class. +""" + +import pytest + +from concrete.numpy.compilation import Compiler + + +def test_compiler_bad_init(helpers): + """ + Test `__init__` method of `Compiler` class with bad parameters. + """ + + configuration = helpers.configuration() + + def f(x, y, z): + return x + y + z + + # missing all + # ----------- + + with pytest.raises(ValueError) as excinfo: + Compiler(f, {}, configuration=configuration) + + assert str(excinfo.value) == ( + "Encryption statuses of parameters 'x', 'y' and 'z' of function 'f' are not provided" + ) + + # missing x and y + # --------------- + + with pytest.raises(ValueError) as excinfo: + Compiler(f, {"z": "clear"}, configuration=configuration) + + assert str(excinfo.value) == ( + "Encryption statuses of parameters 'x' and 'y' of function 'f' are not provided" + ) + + # missing x + # --------- + + with pytest.raises(ValueError) as excinfo: + Compiler(f, {"y": "encrypted", "z": "clear"}, configuration=configuration) + + assert str(excinfo.value) == ( + "Encryption status of parameter 'x' of function 'f' is not provided" + ) + + # additional p + # ------------ + + # this is fine and `p` is just ignored + + Compiler( + f, + {"x": "encrypted", "y": "encrypted", "z": "clear", "p": "clear"}, + configuration=configuration, + ) + + +def test_compiler_bad_call(helpers): + """ + Test `__call__` method of `Compiler` class with bad parameters. + """ + + configuration = helpers.configuration() + + def f(x, y, z): + return x + y + z + + with pytest.raises(RuntimeError) as excinfo: + compiler = Compiler( + f, + {"x": "encrypted", "y": "encrypted", "z": "clear"}, + configuration=configuration, + ) + compiler(1, 2, 3, invalid=4) + + assert str(excinfo.value) == "Calling function 'f' with kwargs is not supported" + + +def test_compiler_bad_trace(helpers): + """ + Test `trace` method of `Compiler` class with bad parameters. + """ + + configuration = helpers.configuration() + + def f(x, y, z): + return x + y + z + + with pytest.raises(RuntimeError) as excinfo: + compiler = Compiler( + f, + {"x": "encrypted", "y": "encrypted", "z": "clear"}, + configuration=configuration, + ) + compiler.trace() + + assert str(excinfo.value) == "Tracing function 'f' without an inputset is not supported" + + +def test_compiler_bad_compile(helpers): + """ + Test `compile` method of `Compiler` class with bad parameters. + """ + + configuration = helpers.configuration() + + def f(x, y, z): + return x + y + z + + with pytest.raises(RuntimeError) as excinfo: + compiler = Compiler( + f, + {"x": "encrypted", "y": "encrypted", "z": "clear"}, + configuration=configuration, + ) + compiler.compile() + + assert str(excinfo.value) == "Compiling function 'f' without an inputset is not supported" diff --git a/tests/compilation/test_configuration.py b/tests/compilation/test_configuration.py new file mode 100644 index 000000000..c04bbde78 --- /dev/null +++ b/tests/compilation/test_configuration.py @@ -0,0 +1,28 @@ +""" +Tests of `CompilationConfiguration` class. +""" + +import pytest + +from concrete.numpy.compilation import CompilationConfiguration + + +@pytest.mark.parametrize( + "kwargs,expected_error,expected_message", + [ + pytest.param( + {"enable_unsafe_features": False, "use_insecure_key_cache": True}, + RuntimeError, + "Insecure key cache cannot be used without enabling unsafe features", + ), + ], +) +def test_configuration_bad_init(kwargs, expected_error, expected_message): + """ + Test `__init__` method of `CompilationConfiguration` class with bad parameters. + """ + + with pytest.raises(expected_error) as excinfo: + CompilationConfiguration(**kwargs) + + assert str(excinfo.value) == expected_message diff --git a/tests/compilation/test_decorator.py b/tests/compilation/test_decorator.py new file mode 100644 index 000000000..417d9ec80 --- /dev/null +++ b/tests/compilation/test_decorator.py @@ -0,0 +1,86 @@ +""" +Tests of `compiler` decorator. +""" + +from concrete.numpy.compilation import CompilationArtifacts, compiler + + +def test_call_compile(helpers): + """ + Test `__call__` and `compile` methods of `compiler` decorator back to back. + """ + + configuration = helpers.configuration() + + @compiler({"x": "encrypted"}, configuration=configuration) + def function(x): + return x + 42 + + for i in range(10): + function(i) + + circuit = function.compile() + + sample = 5 + helpers.check_execution(circuit, function, sample) + + +def test_compiler_verbose_trace(helpers, capsys): + """ + Test `trace` method of `compiler` decorator with verbose flag. + """ + + configuration = helpers.configuration() + artifacts = CompilationArtifacts() + + @compiler({"x": "encrypted"}, configuration=configuration, artifacts=artifacts) + def function(x): + return x + 42 + + inputset = range(10) + function.trace(inputset, show_graph=True) + + captured = capsys.readouterr() + assert captured.out.strip() == ( + f""" + +Computation Graph +------------------------------------------------ +{str(list(artifacts.textual_representations_of_graphs.values())[-1])} +------------------------------------------------ + + """.strip() + ) + + +def test_compiler_verbose_compile(helpers, capsys): + """ + Test `compile` method of `compiler` decorator with verbose flag. + """ + + configuration = helpers.configuration() + artifacts = CompilationArtifacts() + + @compiler({"x": "encrypted"}, configuration=configuration, artifacts=artifacts) + def function(x): + return x + 42 + + inputset = range(10) + function.compile(inputset, show_graph=True, show_mlir=True) + + captured = capsys.readouterr() + assert captured.out.strip() == ( + f""" + +Computation Graph +-------------------------------------------------------------------------------- +{list(artifacts.textual_representations_of_graphs.values())[-1]} +-------------------------------------------------------------------------------- + +MLIR +-------------------------------------------------------------------------------- +{artifacts.mlir_to_compile} +-------------------------------------------------------------------------------- + + """.strip() + ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..749a41ba3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,298 @@ +""" +Configuration of `pytest`. +""" + +import json +import random +from pathlib import Path +from typing import Any, Callable, Dict, List, Tuple, Union + +import numpy as np +import pytest + +import concrete.numpy as cnp +from concrete.numpy.compilation import configuration as configuration_ + + +def pytest_addoption(parser): + """ + Add CLI options. + """ + + parser.addoption( + "--global-coverage", + type=str, + default=None, + action="store", + help="JSON file to dump pytest-cov terminal report.", + ) + parser.addoption( + "--key-cache", + type=str, + default=None, + action="store", + help="Specify the location of the key cache", + ) + + +def pytest_sessionstart(session): + """ + Initialize insecure key cache. + """ + + key_cache_location = session.config.getoption("--key-cache", default=None) + if key_cache_location is not None: + if key_cache_location.lower() == "disable": + key_cache_location = None + else: + key_cache_location = Path(key_cache_location).expanduser().resolve() + else: + key_cache_location = Path.home().resolve() / ".cache" / "concrete-numpy" / "pytest" + + if key_cache_location: + key_cache_location.mkdir(parents=True, exist_ok=True) + print(f"INSECURE_KEY_CACHE_LOCATION={str(key_cache_location)}") + + # pylint: disable=protected-access + configuration_._INSECURE_KEY_CACHE_LOCATION = str(key_cache_location) + # pylint: enable=protected-access + + +def pytest_sessionfinish(session, exitstatus): # pylint: disable=unused-argument + """ + Save global coverage info after testing is finished. + """ + + # 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_option = session.config.getoption("--global-coverage", default=None) + if global_coverage_option is not None: + coverage_plugin = session.config.pluginmanager.getplugin("_cov") + coverage_txt = coverage_plugin.cov_report.getvalue() + + coverage_status = 0 + if ( + coverage_plugin.options.cov_fail_under is not None + and coverage_plugin.options.cov_fail_under > 0 + and coverage_plugin.cov_total < coverage_plugin.options.cov_fail_under + ): + coverage_status = 1 + + global_coverage_file_path = Path(global_coverage_option).resolve() + with open(global_coverage_file_path, "w", encoding="utf-8") as f: + json.dump({"exit_code": coverage_status, "content": coverage_txt}, f) + + +class Helpers: + """ + Helpers class, which provides various helpers to tests. + """ + + @staticmethod + def configuration() -> cnp.CompilationConfiguration: + """ + Get the test configuration to use during testing. + + Returns: + cnp.CompilationConfiguration: + test configuration + """ + + return cnp.CompilationConfiguration( + dump_artifacts_on_unexpected_failures=False, + enable_unsafe_features=True, + use_insecure_key_cache=True, + ) + + @staticmethod + def generate_encryption_statuses(parameters: Dict[str, Dict[str, Any]]) -> Dict[str, str]: + """ + Generate parameter encryption statuses accoring to a parameter specification. + + Args: + parameters (Dict[str, Dict[str, Any]]): + parameter specification to use + + e.g., + + { + "param1": {"range": [0, 10], "status": "clear"}, + "param2": {"range": [3, 30], "status": "encrypted", "shape": (3,)}, + } + + Returns: + Dict[str, str]: + parameter encryption statuses + generated according to the given parameter specification + """ + + return { + parameter: details["status"] if "status" in details else "encrypted" + for parameter, details in parameters.items() + } + + @staticmethod + def generate_inputset( + parameters: Dict[str, Dict[str, Any]], + size: int = 128, + ) -> List[Union[Tuple[Union[int, np.ndarray], ...], Union[int, np.ndarray]]]: + """ + Generate a random inputset of desired size accoring to a parameter specification. + + Args: + parameters (Dict[str, Dict[str, Any]]): + parameter specification to use + + e.g., + + { + "param1": {"range": [0, 10], "status": "clear"}, + "param2": {"range": [3, 30], "status": "encrypted", "shape": (3,)}, + } + + size (int): + size of the resulting inputset + + Returns: + List[Union[Tuple[Union[int, np.ndarray], ...], Union[int, np.ndarray]]]: + random inputset of desired size + generated according to the given parameter specification + """ + + inputset = [] + + for _ in range(size): + sample = Helpers.generate_sample(parameters) + inputset.append(tuple(sample) if len(sample) > 1 else sample[0]) + + return inputset + + @staticmethod + def generate_sample(parameters: Dict[str, Dict[str, Any]]) -> List[Union[int, np.ndarray]]: + """ + Generate a random sample accoring to a parameter specification. + + Args: + parameters (Dict[str, Dict[str, Any]]): + parameter specification to use + + e.g., + + { + "param1": {"range": [0, 10], "status": "clear"}, + "param2": {"range": [3, 30], "status": "encrypted", "shape": (3,)}, + } + + Returns: + List[Union[int, np.ndarray]]: + random sample + generated according to the given parameter specification + """ + + sample = [] + + for description in parameters.values(): + minimum, maximum = description.get("range", [0, 127]) + + assert minimum >= 0 + assert maximum <= 127 + + if "shape" in description: + shape = description["shape"] + sample.append(np.random.randint(minimum, maximum + 1, size=shape, dtype=np.int64)) + else: + sample.append(np.int64(random.randint(minimum, maximum))) + + return sample + + @staticmethod + def check_execution( + circuit: cnp.Circuit, + function: Callable, + sample: Union[Any, List[Any]], + retries: int = 1, + ): + """ + Assert that `circuit` is behaves the same as `function` on `sample`. + + Args: + circuit (cnp.Circuit): + compiled circuit + + function (Callable): + original function + + sample (List[Any]): + inputs + + retries (int): + number of times to retry (for probabilistic execution) + """ + + if not isinstance(sample, list): + sample = [sample] + + for i in range(retries): + expected = function(*sample) + actual = circuit.run(*sample) + + if not isinstance(expected, tuple): + expected = (expected,) + if not isinstance(actual, tuple): + actual = (actual,) + + if all(np.array_equal(e, a) for e, a in zip(expected, actual)): + break + + if i == retries - 1: + raise AssertionError( + f""" + +Expected Output +=============== +{expected} + +Actual Output +============= +{actual} + + """ + ) + + @staticmethod + def check_str(expected: str, actual: str): + """ + Assert that `circuit` is behaves the same as `function` on `sample`. + + Args: + expected (str): + expected str + + actual (str): + actual str + """ + + assert ( + actual.strip() == expected.strip() + ), f""" + +Expected Output +=============== +{expected} + +Actual Output +============= +{actual} + + """ + + +@pytest.fixture +def helpers(): + """ + Fixture that provides `Helpers` class to tests. + """ + + return Helpers diff --git a/tests/dtypes/__init__.py b/tests/dtypes/__init__.py new file mode 100644 index 000000000..d348fda21 --- /dev/null +++ b/tests/dtypes/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of `concrete.numpy.dtypes` namespace. +""" diff --git a/tests/dtypes/test_float.py b/tests/dtypes/test_float.py new file mode 100644 index 000000000..7aa4234ac --- /dev/null +++ b/tests/dtypes/test_float.py @@ -0,0 +1,92 @@ +""" +Tests of `Float` data type. +""" + +import pytest + +from concrete.numpy.dtypes import Float + + +@pytest.mark.parametrize( + "bit_width,expected_error,expected_message", + [ + pytest.param( + 128, + ValueError, + "Float(128) is not supported (bit width must be one of 16, 32 or 64)", + ), + pytest.param( + "abc", + ValueError, + "Float('abc') is not supported (bit width must be one of 16, 32 or 64)", + ), + ], +) +def test_float_bad_init(bit_width, expected_error, expected_message): + """ + Test `__init__` method of `Float` data type with bad parameters. + """ + + with pytest.raises(expected_error) as excinfo: + Float(bit_width) + + assert str(excinfo.value) == expected_message + + +@pytest.mark.parametrize( + "lhs,rhs,expected_result", + [ + pytest.param( + Float(32), + Float(32), + True, + ), + pytest.param( + Float(32), + Float(64), + False, + ), + pytest.param( + Float(32), + "Float(32)", + False, + ), + pytest.param( + "Float(32)", + Float(32), + False, + ), + ], +) +def test_float_eq(lhs, rhs, expected_result): + """ + Test `__eq__` method of `Float` data type. + """ + + assert (lhs == rhs) == expected_result + assert (rhs == lhs) == expected_result + + +@pytest.mark.parametrize( + "data_type,expected_result", + [ + pytest.param( + Float(16), + "float16", + ), + pytest.param( + Float(32), + "float32", + ), + pytest.param( + Float(64), + "float64", + ), + ], +) +def test_float_str(data_type, expected_result): + """ + Test `__str__` method of `Float` data type. + """ + + assert str(data_type) == expected_result diff --git a/tests/dtypes/test_integer.py b/tests/dtypes/test_integer.py new file mode 100644 index 000000000..aa88d2596 --- /dev/null +++ b/tests/dtypes/test_integer.py @@ -0,0 +1,691 @@ +""" +Tests of `Integer` data type. +""" + +import numpy as np +import pytest + +from concrete.numpy.dtypes import Integer, SignedInteger, UnsignedInteger + + +@pytest.mark.parametrize( + "value,force_signed,expected_result", + [ + pytest.param( + -4, + False, + SignedInteger(3), + ), + pytest.param( + -3, + False, + SignedInteger(3), + ), + pytest.param( + -2, + False, + SignedInteger(2), + ), + pytest.param( + -1, + False, + SignedInteger(1), + ), + pytest.param( + 0, + False, + UnsignedInteger(1), + ), + pytest.param( + 1, + False, + UnsignedInteger(1), + ), + pytest.param( + 2, + False, + UnsignedInteger(2), + ), + pytest.param( + 3, + False, + UnsignedInteger(2), + ), + pytest.param( + 4, + False, + UnsignedInteger(3), + ), + pytest.param( + -4, + True, + SignedInteger(3), + ), + pytest.param( + -3, + True, + SignedInteger(3), + ), + pytest.param( + -2, + True, + SignedInteger(2), + ), + pytest.param( + -1, + True, + SignedInteger(1), + ), + pytest.param( + 0, + True, + SignedInteger(1), + ), + pytest.param( + 1, + True, + SignedInteger(2), + ), + pytest.param( + 2, + True, + SignedInteger(3), + ), + pytest.param( + 3, + True, + SignedInteger(3), + ), + pytest.param( + 4, + True, + SignedInteger(4), + ), + pytest.param( + np.array([0, 1]), + False, + UnsignedInteger(1), + ), + pytest.param( + np.array([0, 1]), + True, + SignedInteger(2), + ), + pytest.param( + [-1, 1], + False, + SignedInteger(2), + ), + pytest.param( + [-1, 1], + True, + SignedInteger(2), + ), + ], +) +def test_integer_that_can_represent(value, force_signed, expected_result): + """ + Test `that_can_represent` function of `Integer` data type. + """ + + assert Integer.that_can_represent(value, force_signed) == expected_result + + +@pytest.mark.parametrize( + "value,force_signed,expected_error,expected_message", + [ + pytest.param( + "abc", + False, + ValueError, + "Integer cannot represent 'abc'", + ), + pytest.param( + "abc", + True, + ValueError, + "Integer cannot represent 'abc'", + ), + pytest.param( + 4.2, + False, + ValueError, + "Integer cannot represent 4.2", + ), + pytest.param( + 4.2, + True, + ValueError, + "Integer cannot represent 4.2", + ), + pytest.param( + np.array([2.2, 1.1]), + False, + ValueError, + "Integer cannot represent array([2.2, 1.1])", + ), + pytest.param( + np.array([2.2, 1.1]), + True, + ValueError, + "Integer cannot represent array([2.2, 1.1])", + ), + pytest.param( + [1, (), 3], + True, + ValueError, + "Integer cannot represent [1, (), 3]", + ), + ], +) +def test_integer_bad_that_can_represent(value, force_signed, expected_error, expected_message): + """ + Test `that_can_represent` function of `Integer` data type with bad parameters. + """ + + with pytest.raises(expected_error) as excinfo: + Integer.that_can_represent(value, force_signed) + + assert str(excinfo.value) == expected_message + + +@pytest.mark.parametrize( + "constructor,bit_width,expected_error,expected_message", + [ + pytest.param( + SignedInteger, + 0, + ValueError, + "SignedInteger(0) is not supported (bit width must be a positive integer)", + ), + pytest.param( + UnsignedInteger, + 0, + ValueError, + "UnsignedInteger(0) is not supported (bit width must be a positive integer)", + ), + pytest.param( + SignedInteger, + -1, + ValueError, + "SignedInteger(-1) is not supported (bit width must be a positive integer)", + ), + pytest.param( + UnsignedInteger, + -1, + ValueError, + "UnsignedInteger(-1) is not supported (bit width must be a positive integer)", + ), + pytest.param( + SignedInteger, + "abc", + ValueError, + "SignedInteger('abc') is not supported (bit width must be a positive integer)", + ), + pytest.param( + UnsignedInteger, + "abc", + ValueError, + "UnsignedInteger('abc') is not supported (bit width must be a positive integer)", + ), + ], +) +def test_integer_bad_init(constructor, bit_width, expected_error, expected_message): + """ + Test `__init__` method of `Integer` data type with bad parameters. + """ + + with pytest.raises(expected_error) as excinfo: + constructor(bit_width) + + assert str(excinfo.value) == expected_message + + +@pytest.mark.parametrize( + "lhs,rhs,expected_result", + [ + pytest.param( + SignedInteger(5), + SignedInteger(5), + True, + ), + pytest.param( + UnsignedInteger(5), + UnsignedInteger(5), + True, + ), + pytest.param( + SignedInteger(5), + SignedInteger(6), + False, + ), + pytest.param( + SignedInteger(6), + SignedInteger(5), + False, + ), + pytest.param( + UnsignedInteger(5), + UnsignedInteger(6), + False, + ), + pytest.param( + UnsignedInteger(6), + UnsignedInteger(5), + False, + ), + pytest.param( + SignedInteger(5), + UnsignedInteger(5), + False, + ), + pytest.param( + UnsignedInteger(5), + SignedInteger(5), + False, + ), + pytest.param( + SignedInteger(5), + "SignedInteger(5)", + False, + ), + pytest.param( + "SignedInteger(5)", + SignedInteger(5), + False, + ), + ], +) +def test_integer_eq(lhs, rhs, expected_result): + """ + Test `__eq__` method of `Integer` data type. + """ + + assert (lhs == rhs) == expected_result + assert (rhs == lhs) == expected_result + + +@pytest.mark.parametrize( + "data_type,expected_result", + [ + pytest.param( + UnsignedInteger(4), + "uint4", + ), + pytest.param( + UnsignedInteger(7), + "uint7", + ), + pytest.param( + SignedInteger(4), + "int4", + ), + pytest.param( + SignedInteger(7), + "int7", + ), + ], +) +def test_integer_str(data_type, expected_result): + """ + Test `__str__` method of `Integer` data type. + """ + + assert str(data_type) == expected_result + + +@pytest.mark.parametrize( + "data_type,expected_result", + [ + pytest.param( + UnsignedInteger(1), + 0, + ), + pytest.param( + UnsignedInteger(3), + 0, + ), + pytest.param( + UnsignedInteger(5), + 0, + ), + pytest.param( + SignedInteger(1), + -1, + ), + pytest.param( + SignedInteger(3), + -4, + ), + pytest.param( + SignedInteger(5), + -16, + ), + ], +) +def test_integer_min(data_type, expected_result): + """ + Test `min` method of `Integer` data type. + """ + + assert data_type.min() == expected_result + + +@pytest.mark.parametrize( + "data_type,expected_result", + [ + pytest.param( + UnsignedInteger(1), + 1, + ), + pytest.param( + UnsignedInteger(3), + 7, + ), + pytest.param( + UnsignedInteger(5), + 31, + ), + pytest.param( + SignedInteger(1), + 0, + ), + pytest.param( + SignedInteger(3), + 3, + ), + pytest.param( + SignedInteger(5), + 15, + ), + ], +) +def test_integer_max(data_type, expected_result): + """ + Test `max` method of `Integer` data type. + """ + + assert data_type.max() == expected_result + + +@pytest.mark.parametrize( + "data_type,value,expected_result", + [ + pytest.param( + UnsignedInteger(1), + -4, + False, + ), + pytest.param( + UnsignedInteger(1), + -3, + False, + ), + pytest.param( + UnsignedInteger(1), + -2, + False, + ), + pytest.param( + UnsignedInteger(1), + -1, + False, + ), + pytest.param( + UnsignedInteger(1), + 0, + True, + ), + pytest.param( + UnsignedInteger(1), + 1, + True, + ), + pytest.param( + UnsignedInteger(1), + 2, + False, + ), + pytest.param( + UnsignedInteger(1), + 3, + False, + ), + pytest.param( + UnsignedInteger(1), + 4, + False, + ), + pytest.param( + UnsignedInteger(2), + -4, + False, + ), + pytest.param( + UnsignedInteger(2), + -3, + False, + ), + pytest.param( + UnsignedInteger(2), + -2, + False, + ), + pytest.param( + UnsignedInteger(2), + -1, + False, + ), + pytest.param( + UnsignedInteger(2), + 0, + True, + ), + pytest.param( + UnsignedInteger(2), + 1, + True, + ), + pytest.param( + UnsignedInteger(2), + 2, + True, + ), + pytest.param( + UnsignedInteger(2), + 3, + True, + ), + pytest.param( + UnsignedInteger(2), + 4, + False, + ), + pytest.param( + UnsignedInteger(3), + -4, + False, + ), + pytest.param( + UnsignedInteger(3), + -3, + False, + ), + pytest.param( + UnsignedInteger(3), + -2, + False, + ), + pytest.param( + UnsignedInteger(3), + -1, + False, + ), + pytest.param( + UnsignedInteger(3), + 0, + True, + ), + pytest.param( + UnsignedInteger(3), + 1, + True, + ), + pytest.param( + UnsignedInteger(3), + 2, + True, + ), + pytest.param( + UnsignedInteger(3), + 3, + True, + ), + pytest.param( + UnsignedInteger(3), + 4, + True, + ), + pytest.param( + SignedInteger(1), + -4, + False, + ), + pytest.param( + SignedInteger(1), + -3, + False, + ), + pytest.param( + SignedInteger(1), + -2, + False, + ), + pytest.param( + SignedInteger(1), + -1, + True, + ), + pytest.param( + SignedInteger(1), + 0, + True, + ), + pytest.param( + SignedInteger(1), + 1, + False, + ), + pytest.param( + SignedInteger(1), + 2, + False, + ), + pytest.param( + SignedInteger(1), + 3, + False, + ), + pytest.param( + SignedInteger(1), + 4, + False, + ), + pytest.param( + SignedInteger(2), + -4, + False, + ), + pytest.param( + SignedInteger(2), + -3, + False, + ), + pytest.param( + SignedInteger(2), + -2, + True, + ), + pytest.param( + SignedInteger(2), + -1, + True, + ), + pytest.param( + SignedInteger(2), + 0, + True, + ), + pytest.param( + SignedInteger(2), + 1, + True, + ), + pytest.param( + SignedInteger(2), + 2, + False, + ), + pytest.param( + SignedInteger(2), + 3, + False, + ), + pytest.param( + SignedInteger(2), + 4, + False, + ), + pytest.param( + SignedInteger(3), + -4, + True, + ), + pytest.param( + SignedInteger(3), + -3, + True, + ), + pytest.param( + SignedInteger(3), + -2, + True, + ), + pytest.param( + SignedInteger(3), + -1, + True, + ), + pytest.param( + SignedInteger(3), + 0, + True, + ), + pytest.param( + SignedInteger(3), + 1, + True, + ), + pytest.param( + SignedInteger(3), + 2, + True, + ), + pytest.param( + SignedInteger(3), + 3, + True, + ), + pytest.param( + SignedInteger(3), + 4, + False, + ), + ], +) +def test_integer_can_represent(data_type, value, expected_result): + """ + Test `can_represent` method of `Integer` data type. + """ + + assert data_type.can_represent(value) == expected_result diff --git a/tests/dtypes/test_utils.py b/tests/dtypes/test_utils.py new file mode 100644 index 000000000..d427fef14 --- /dev/null +++ b/tests/dtypes/test_utils.py @@ -0,0 +1,77 @@ +""" +Tests of utilities related to data types. +""" + +import pytest + +from concrete.numpy.dtypes import Float, SignedInteger, UnsignedInteger +from concrete.numpy.dtypes.utils import combine_dtypes + + +@pytest.mark.parametrize( + "dtypes,expected_result", + [ + pytest.param( + [Float(64), Float(64)], + Float(64), + ), + pytest.param( + [Float(32), Float(64)], + Float(64), + ), + pytest.param( + [Float(16), Float(64)], + Float(64), + ), + pytest.param( + [Float(32), Float(16)], + Float(32), + ), + pytest.param( + [Float(16), Float(16)], + Float(16), + ), + pytest.param( + [SignedInteger(5), Float(64)], + Float(64), + ), + pytest.param( + [Float(32), SignedInteger(5)], + Float(32), + ), + pytest.param( + [SignedInteger(5), Float(16)], + Float(16), + ), + pytest.param( + [SignedInteger(5), SignedInteger(6)], + SignedInteger(6), + ), + pytest.param( + [UnsignedInteger(5), UnsignedInteger(6)], + UnsignedInteger(6), + ), + pytest.param( + [SignedInteger(5), UnsignedInteger(6)], + SignedInteger(7), + ), + pytest.param( + [SignedInteger(5), UnsignedInteger(4)], + SignedInteger(5), + ), + pytest.param( + [UnsignedInteger(6), SignedInteger(5)], + SignedInteger(7), + ), + pytest.param( + [UnsignedInteger(4), SignedInteger(5)], + SignedInteger(5), + ), + ], +) +def test_combine_dtypes(dtypes, expected_result): + """ + Test `combine_dtypes` function. + """ + + assert combine_dtypes(dtypes) == expected_result diff --git a/tests/execution/__init__.py b/tests/execution/__init__.py new file mode 100644 index 000000000..5d8e8e694 --- /dev/null +++ b/tests/execution/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of execution. +""" diff --git a/tests/execution/test_add.py b/tests/execution/test_add.py new file mode 100644 index 000000000..7a0abef00 --- /dev/null +++ b/tests/execution/test_add.py @@ -0,0 +1,159 @@ +""" +Tests of execution of add operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "function", + [ + pytest.param( + lambda x: x + 42, + id="x + 42", + ), + pytest.param( + lambda x: 42 + x, + id="42 + x", + ), + pytest.param( + lambda x: x + np.array([1, 2, 3]), + id="x + [1, 2, 3]", + ), + pytest.param( + lambda x: np.array([1, 2, 3]) + x, + id="[1, 2, 3] + x", + ), + pytest.param( + lambda x: x + np.array([[1, 2, 3], [4, 5, 6]]), + id="x + [[1, 2, 3], [4, 5, 6]]", + ), + pytest.param( + lambda x: np.array([[1, 2, 3], [4, 5, 6]]) + x, + id="[[1, 2, 3], [4, 5, 6]] + x", + ), + ], +) +@pytest.mark.parametrize( + "parameters", + [ + { + "x": {"range": [0, 85], "status": "encrypted"}, + }, + { + "x": {"range": [0, 85], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 85], "status": "encrypted", "shape": (2, 3)}, + }, + ], +) +def test_constant_add(function, parameters, helpers): + """ + Test add where one of the operators is a constant. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample) + + +@pytest.mark.parametrize( + "function", + [ + pytest.param( + lambda x, y: x + y, + id="x + y", + ), + ], +) +@pytest.mark.parametrize( + "parameters", + [ + { + "x": {"range": [0, 60], "status": "clear"}, + "y": {"range": [0, 60], "status": "encrypted"}, + }, + { + "x": {"range": [0, 60], "status": "encrypted"}, + "y": {"range": [0, 60], "status": "clear"}, + }, + { + "x": {"range": [0, 60], "status": "encrypted"}, + "y": {"range": [0, 60], "status": "encrypted"}, + }, + { + "x": {"range": [0, 60], "status": "clear", "shape": (3,)}, + "y": {"range": [0, 60], "status": "encrypted"}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + "y": {"range": [0, 60], "status": "clear"}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + "y": {"range": [0, 60], "status": "encrypted"}, + }, + { + "x": {"range": [0, 60], "status": "clear"}, + "y": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted"}, + "y": {"range": [0, 60], "status": "clear", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted"}, + "y": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "clear", "shape": (3,)}, + "y": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + "y": {"range": [0, 60], "status": "clear", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + "y": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "clear", "shape": (2, 1)}, + "y": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (2, 1)}, + "y": {"range": [0, 60], "status": "clear", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (2, 1)}, + "y": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + ], +) +def test_add(function, parameters, helpers): + """ + Test add where both of the operators are dynamic. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample) diff --git a/tests/execution/test_concat.py b/tests/execution/test_concat.py new file mode 100644 index 000000000..6a42f33fa --- /dev/null +++ b/tests/execution/test_concat.py @@ -0,0 +1,176 @@ +""" +Tests of execution of concatenate operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "function,parameters", + [ + pytest.param( + lambda x, y: np.concatenate((x, y)), + { + "x": {"shape": (4, 2)}, + "y": {"shape": (3, 2)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=0), + { + "x": {"shape": (4, 2)}, + "y": {"shape": (3, 2)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=1), + { + "x": {"shape": (2, 4)}, + "y": {"shape": (2, 3)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=-1), + { + "x": {"shape": (2, 4)}, + "y": {"shape": (2, 3)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=-2), + { + "x": {"shape": (4, 2)}, + "y": {"shape": (3, 2)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=None), + { + "x": {"shape": (3, 4)}, + "y": {"shape": (2, 3)}, + }, + ), + pytest.param( + lambda x, y, z: np.concatenate((x, y, z)), + { + "x": {"shape": (4, 2)}, + "y": {"shape": (3, 2)}, + "z": {"shape": (5, 2)}, + }, + ), + pytest.param( + lambda x, y, z: np.concatenate((x, y, z), axis=0), + { + "x": {"shape": (4, 2)}, + "y": {"shape": (3, 2)}, + "z": {"shape": (5, 2)}, + }, + ), + pytest.param( + lambda x, y, z: np.concatenate((x, y, z), axis=1), + { + "x": {"shape": (2, 4)}, + "y": {"shape": (2, 3)}, + "z": {"shape": (2, 5)}, + }, + ), + pytest.param( + lambda x, y, z: np.concatenate((x, y, z), axis=-1), + { + "x": {"shape": (2, 4)}, + "y": {"shape": (2, 3)}, + "z": {"shape": (2, 5)}, + }, + ), + pytest.param( + lambda x, y, z: np.concatenate((x, y, z), axis=-2), + { + "x": {"shape": (4, 2)}, + "y": {"shape": (3, 2)}, + "z": {"shape": (5, 2)}, + }, + ), + pytest.param( + lambda x, y, z: np.concatenate((x, y, z), axis=None), + { + "x": {"shape": (3, 4)}, + "y": {"shape": (2, 3)}, + "z": {"shape": (5, 1)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y)), + { + "x": {"shape": (3, 4, 2)}, + "y": {"shape": (5, 4, 2)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=0), + { + "x": {"shape": (3, 4, 2)}, + "y": {"shape": (5, 4, 2)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=1), + { + "x": {"shape": (2, 4, 5)}, + "y": {"shape": (2, 3, 5)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=2), + { + "x": {"shape": (2, 3, 4)}, + "y": {"shape": (2, 3, 5)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=-1), + { + "x": {"shape": (2, 3, 4)}, + "y": {"shape": (2, 3, 5)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=-2), + { + "x": {"shape": (2, 4, 5)}, + "y": {"shape": (2, 3, 5)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=-3), + { + "x": {"shape": (3, 4, 2)}, + "y": {"shape": (5, 4, 2)}, + }, + ), + pytest.param( + lambda x, y: np.concatenate((x, y), axis=None), + { + "x": {"shape": (3, 4, 5)}, + "y": {"shape": (5, 2, 3)}, + }, + ), + ], +) +def test_concatenate(function, parameters, helpers): + """ + Test concatenate. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample) diff --git a/tests/execution/test_convolution.py b/tests/execution/test_convolution.py new file mode 100644 index 000000000..efb23a7eb --- /dev/null +++ b/tests/execution/test_convolution.py @@ -0,0 +1,206 @@ +""" +Tests of execution of convolution operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "input_shape,weight_shape", + [ + pytest.param( + (1, 1, 4, 4), + (1, 1, 2, 2), + ), + pytest.param( + (4, 3, 4, 4), + (2, 3, 2, 2), + ), + ], +) +@pytest.mark.parametrize( + "strides", + [ + (2, 2), + ], +) +@pytest.mark.parametrize( + "dilations", + [ + (1, 1), + ], +) +@pytest.mark.parametrize( + "has_bias", + [ + True, + False, + ], +) +def test_conv2d(input_shape, weight_shape, strides, dilations, has_bias, helpers): + """ + Test conv2d. + """ + + configuration = helpers.configuration() + + weight = np.random.randint(0, 4, size=weight_shape) + + if has_bias: + bias = np.random.randint(0, 4, size=(weight_shape[0],)) + else: + bias = None + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def function(x): + return cnp.conv2d(x, weight, bias, strides=strides, dilations=dilations) + + inputset = [np.random.randint(0, 4, size=input_shape) for i in range(100)] + circuit = function.compile(inputset) + + sample = np.random.randint(0, 4, size=input_shape, dtype=np.uint8) + helpers.check_execution(circuit, function, sample) + + +@pytest.mark.parametrize( + "input_shape,weight_shape,bias_shape,pads,strides,dilations,auto_pad," + "expected_error,expected_message", + [ + pytest.param( + (1, 1, 4, 4), + (1, 1, 2, 2), + (1,), + (0, 0, 0, 0), + (1, 1), + (1, 1), + "VALID", + ValueError, + "Auto pad should be in {'NOTSET'} but it's 'VALID'", + ), + pytest.param( + (1, 1, 4, 4), + (1, 1, 2, 2), + (1,), + (), + (1, 1), + (1, 1), + "NOTSET", + ValueError, + "Pads should be of form " + "(height_begin_pad, width_begin_pad, height_end_pad, width_end_pad) " + "but it's ()", + ), + pytest.param( + (1, 1, 4, 4), + (1, 1, 2, 2), + (1,), + (0, 0, 0, 0), + (), + (1, 1), + "NOTSET", + ValueError, + "Strides should be of form (height_stride, width_stride) but it's ()", + ), + pytest.param( + (1, 1, 4, 4), + (1, 1, 2, 2), + (1,), + (0, 0, 0, 0), + (1, 1), + (), + "NOTSET", + ValueError, + "Dilations should be of form (height_dilation, width_dilation) but it's ()", + ), + pytest.param( + (), + (1, 1, 2, 2), + (1,), + (0, 0, 0, 0), + (1, 1), + (1, 1), + "NOTSET", + ValueError, + "Input should be of shape (N, C, H, W) but it's of shape ()", + ), + pytest.param( + (1, 1, 4, 4), + (), + (1,), + (0, 0, 0, 0), + (1, 1), + (1, 1), + "NOTSET", + ValueError, + "Weight should be of shape (F, C, H, W) but it's of shape ()", + ), + pytest.param( + (1, 1, 4, 4), + (1, 1, 2, 2), + (), + (0, 0, 0, 0), + (1, 1), + (1, 1), + "NOTSET", + ValueError, + "Bias should be of shape (F,) but it's of shape ()", + ), + ], +) +def test_bad_conv2d_tracing( + input_shape, + weight_shape, + bias_shape, + pads, + strides, + dilations, + auto_pad, + expected_error, + expected_message, + helpers, +): + """ + Test conv2d with bad parameters. + """ + + configuration = helpers.configuration() + + weight = np.random.randint(0, 4, size=weight_shape) + + if bias_shape is not None: + bias = np.random.randint(0, 4, size=bias_shape) + else: + bias = None + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def function(x): + return cnp.conv2d(x, weight, bias, pads, strides, dilations, auto_pad) + + inputset = [np.random.randint(0, 4, size=input_shape) for i in range(100)] + with pytest.raises(expected_error) as excinfo: + function.compile(inputset) + + assert str(excinfo.value) == expected_message + + +def test_bad_conv2d_evaluation(): + """ + Test conv2d evaluation with bad parameters. + """ + + x = np.random.randint(0, 4, size=(1, 1, 4, 4)) + + with pytest.raises(ValueError) as excinfo: + cnp.conv2d(x, "abc") + + assert str(excinfo.value) == "Weight should be of type np.ndarray for evaluation" + + weight = np.random.randint(0, 4, size=(1, 1, 2, 2)) + + with pytest.raises(ValueError) as excinfo: + cnp.conv2d(x, weight, "abc") + + assert str(excinfo.value) == "Bias should be of type np.ndarray for evaluation" diff --git a/tests/execution/test_direct_table_lookup.py b/tests/execution/test_direct_table_lookup.py new file mode 100644 index 000000000..60f866446 --- /dev/null +++ b/tests/execution/test_direct_table_lookup.py @@ -0,0 +1,299 @@ +""" +Tests of execution of direct table lookup operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +def identity_table_lookup_generator(n): + """ + Get identity table lookup function. + """ + + return lambda x: cnp.LookupTable(range(2 ** n))[x] + + +def random_table_lookup_1b(x): + """ + Lookup on a random table with 1-bit input. + """ + + # fmt: off + table = cnp.LookupTable([10, 12]) + # fmt: on + + return table[x] + + +def random_table_lookup_2b(x): + """ + Lookup on a random table with 2-bit input. + """ + + # fmt: off + table = cnp.LookupTable([3, 8, 22, 127]) + # fmt: on + + return table[x] + + +def random_table_lookup_3b(x): + """ + Lookup on a random table with 3-bit input. + """ + + # fmt: off + table = cnp.LookupTable([30, 52, 125, 23, 17, 12, 90, 4]) + # fmt: on + + return table[x] + + +def random_table_lookup_4b(x): + """ + Lookup on a random table with 4-bit input. + """ + + # fmt: off + table = cnp.LookupTable([30, 52, 125, 23, 17, 12, 90, 4, 21, 51, 22, 15, 53, 100, 75, 90]) + # fmt: on + + return table[x] + + +def random_table_lookup_5b(x): + """ + Lookup on a random table with 5-bit input. + """ + + # fmt: off + table = cnp.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_table_lookup_6b(x): + """ + Lookup on a random table with 6-bit input. + """ + + # fmt: off + table = cnp.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_table_lookup_7b(x): + """ + Lookup on a random table with 7-bit input. + """ + + # fmt: off + table = cnp.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] + + +def negative_identity_table_lookup_generator(n): + """ + Get negative identity table lookup function. + """ + + return lambda x: cnp.LookupTable([-i for i in range(2 ** n)])[x] + + +@pytest.mark.parametrize( + "bits,function", + [ + pytest.param(1, identity_table_lookup_generator(1)), + pytest.param(2, identity_table_lookup_generator(2)), + pytest.param(3, identity_table_lookup_generator(3)), + pytest.param(4, identity_table_lookup_generator(4)), + pytest.param(5, identity_table_lookup_generator(5)), + pytest.param(6, identity_table_lookup_generator(6)), + pytest.param(7, identity_table_lookup_generator(7)), + pytest.param(1, random_table_lookup_1b), + pytest.param(2, random_table_lookup_2b), + pytest.param(3, random_table_lookup_3b), + pytest.param(4, random_table_lookup_4b), + pytest.param(5, random_table_lookup_5b), + pytest.param(6, random_table_lookup_6b), + pytest.param(7, random_table_lookup_7b), + pytest.param(1, negative_identity_table_lookup_generator(1)), + pytest.param(2, negative_identity_table_lookup_generator(2)), + pytest.param(3, negative_identity_table_lookup_generator(3)), + pytest.param(4, negative_identity_table_lookup_generator(4)), + pytest.param(5, negative_identity_table_lookup_generator(5)), + pytest.param(6, negative_identity_table_lookup_generator(6)), + ], +) +def test_direct_table_lookup(bits, function, helpers): + """ + Test direct table lookup. + """ + + configuration = helpers.configuration() + + # scalar + # ------ + + compiler = cnp.Compiler(function, {"x": "encrypted"}, configuration) + + inputset = range(2 ** bits) + circuit = compiler.compile(inputset) + + sample = int(np.random.randint(0, 2 ** bits)) + helpers.check_execution(circuit, function, sample, retries=10) + + # tensor + # ------ + + compiler = cnp.Compiler(function, {"x": "encrypted"}, configuration) + + inputset = [np.random.randint(0, 2 ** bits, size=(3, 2), dtype=np.uint8) for _ in range(100)] + circuit = compiler.compile(inputset) + + sample = np.random.randint(0, 2 ** bits, size=(3, 2), dtype=np.uint8) + helpers.check_execution(circuit, function, sample, retries=10) + + +def test_direct_multi_table_lookup(helpers): + """ + Test direct multi table lookup. + """ + + configuration = helpers.configuration() + + square = cnp.LookupTable([i * i for i in range(4)]) + cube = cnp.LookupTable([i * i * i for i in range(4)]) + + table = cnp.LookupTable( + [ + [square, cube], + [cube, square], + [square, cube], + ] + ) + + def function(x): + return table[x] + + compiler = cnp.Compiler(function, {"x": "encrypted"}, configuration) + + inputset = [np.random.randint(0, 2 ** 2, size=(3, 2), dtype=np.uint8) for _ in range(100)] + circuit = compiler.compile(inputset) + + sample = np.random.randint(0, 2 ** 2, size=(3, 2), dtype=np.uint8) + helpers.check_execution(circuit, function, sample, retries=10) + + +def test_bad_direct_table_lookup(helpers): + """ + Test direct table lookup with bad parameters. + """ + + configuration = helpers.configuration() + + # empty table + # ----------- + + with pytest.raises(ValueError) as excinfo: + cnp.LookupTable([]) + + assert str(excinfo.value) == "LookupTable cannot be constructed with []" + + # invalid table + # ------------- + + with pytest.raises(ValueError) as excinfo: + cnp.LookupTable([[0, 1], [2, 3]]) + + assert str(excinfo.value) == "LookupTable cannot be constructed with [[0, 1], [2, 3]]" + + # invalid multi table + # ------------------- + + with pytest.raises(ValueError) as excinfo: + cnp.LookupTable(["abc", 3.2]) + + assert str(excinfo.value) == "LookupTable cannot be constructed with ['abc', 3.2]" + + # simulation with float value + # --------------------------- + + with pytest.raises(ValueError) as excinfo: + random_table_lookup_3b(1.1) + + assert str(excinfo.value) == "LookupTable cannot be looked up with 1.1" + + # simulation with invalid shape + # ----------------------------- + + square = cnp.LookupTable([i * i for i in range(4)]) + cube = cnp.LookupTable([i * i * i for i in range(4)]) + + table = cnp.LookupTable( + [ + [square, cube], + [cube, square], + [square, cube], + ] + ) + + with pytest.raises(ValueError) as excinfo: + _ = table[np.array([1, 2])] + + assert str(excinfo.value) == "LookupTable of shape (3, 2) cannot be looked up with [1 2]" + + # compilation with float value + # ---------------------------- + + compiler = cnp.Compiler(random_table_lookup_3b, {"x": "encrypted"}, configuration) + + inputset = [1.5] + with pytest.raises(ValueError) as excinfo: + compiler.compile(inputset) + + assert str(excinfo.value) == "LookupTable cannot be looked up with EncryptedScalar" + + # compilation with invalid shape + # ------------------------------ + + compiler = cnp.Compiler(lambda x: table[x], {"x": "encrypted"}, configuration) + + inputset = [10, 5, 6, 2] + with pytest.raises(ValueError) as excinfo: + compiler.compile(inputset) + + assert str(excinfo.value) == ( + "LookupTable of shape (3, 2) cannot be looked up with EncryptedScalar" + ) diff --git a/tests/execution/test_dot.py b/tests/execution/test_dot.py new file mode 100644 index 000000000..d9c1b05c1 --- /dev/null +++ b/tests/execution/test_dot.py @@ -0,0 +1,47 @@ +""" +Tests of execution of dot operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "size", + [1, 4, 6, 10], +) +def test_dot(size, helpers): + """ + Test dot. + """ + + configuration = helpers.configuration() + + bound = int(np.floor(np.sqrt(127 / size))) + cst = np.random.randint(0, bound, size=(size,)) + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def left_function(x): + return np.dot(x, cst) + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def right_function(x): + return np.dot(cst, x) + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def method(x): + return x.dot(cst) + + inputset = [np.random.randint(0, bound, size=(size,)) for i in range(100)] + + left_function_circuit = left_function.compile(inputset) + right_function_circuit = right_function.compile(inputset) + method_circuit = method.compile(inputset) + + sample = np.random.randint(0, bound, size=(size,), dtype=np.uint8) + + helpers.check_execution(left_function_circuit, left_function, sample) + helpers.check_execution(right_function_circuit, right_function, sample) + helpers.check_execution(method_circuit, method, sample) diff --git a/tests/execution/test_matmul.py b/tests/execution/test_matmul.py new file mode 100644 index 000000000..b035a061b --- /dev/null +++ b/tests/execution/test_matmul.py @@ -0,0 +1,158 @@ +""" +Tests of execution of matmul operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "lhs_shape,rhs_shape,bounds", + [ + pytest.param( + (3, 2), + (2, 3), + (0, 3), + ), + pytest.param( + (1, 2), + (2, 1), + (0, 3), + ), + pytest.param( + (3, 3), + (3, 3), + (0, 3), + ), + pytest.param( + (2, 1), + (1, 2), + (0, 7), + ), + pytest.param( + (2,), + (2,), + (0, 7), + ), + pytest.param( + (5, 5), + (5,), + (0, 3), + ), + pytest.param( + (5,), + (5, 5), + (0, 3), + ), + pytest.param( + (5,), + (5, 3), + (0, 3), + ), + pytest.param( + (5, 3), + (3,), + (0, 3), + ), + pytest.param( + (5,), + (4, 5, 3), + (0, 5), + ), + pytest.param( + (4, 5, 3), + (3,), + (0, 5), + ), + pytest.param( + (5,), + (2, 4, 5, 3), + (0, 5), + ), + pytest.param( + (2, 4, 5, 3), + (3,), + (0, 5), + ), + pytest.param( + (5, 4, 3), + (3, 2), + (0, 5), + ), + pytest.param( + (4, 3), + (5, 3, 2), + (0, 5), + ), + pytest.param( + (2, 5, 4, 3), + (3, 2), + (0, 5), + ), + pytest.param( + (5, 4, 3), + (1, 3, 2), + (0, 5), + ), + pytest.param( + (1, 4, 3), + (5, 3, 2), + (0, 5), + ), + pytest.param( + (5, 4, 3), + (2, 1, 3, 2), + (0, 5), + ), + pytest.param( + (2, 1, 4, 3), + (5, 3, 2), + (0, 5), + ), + ], +) +def test_matmul(lhs_shape, rhs_shape, bounds, helpers): + """ + Test matmul. + """ + + configuration = helpers.configuration() + + minimum, maximum = bounds + + lhs_cst = list(np.random.randint(minimum, maximum, size=lhs_shape)) + rhs_cst = list(np.random.randint(minimum, maximum, size=rhs_shape)) + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def lhs_operator(x): + return x @ rhs_cst + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def rhs_operator(x): + return lhs_cst @ x + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def lhs_function(x): + return np.matmul(x, rhs_cst) + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def rhs_function(x): + return np.matmul(lhs_cst, x) + + lhs_inputset = [np.random.randint(minimum, maximum, size=lhs_shape) for i in range(100)] + rhs_inputset = [np.random.randint(minimum, maximum, size=rhs_shape) for i in range(100)] + + lhs_operator_circuit = lhs_operator.compile(lhs_inputset) + rhs_operator_circuit = rhs_operator.compile(rhs_inputset) + lhs_function_circuit = lhs_function.compile(lhs_inputset) + rhs_function_circuit = rhs_function.compile(rhs_inputset) + + lhs_sample = np.random.randint(minimum, maximum, size=lhs_shape, dtype=np.uint8) + rhs_sample = np.random.randint(minimum, maximum, size=rhs_shape, dtype=np.uint8) + + helpers.check_execution(lhs_operator_circuit, lhs_operator, lhs_sample) + helpers.check_execution(rhs_operator_circuit, rhs_operator, rhs_sample) + helpers.check_execution(lhs_function_circuit, lhs_function, lhs_sample) + helpers.check_execution(rhs_function_circuit, rhs_function, rhs_sample) diff --git a/tests/execution/test_mul.py b/tests/execution/test_mul.py new file mode 100644 index 000000000..77af5fd33 --- /dev/null +++ b/tests/execution/test_mul.py @@ -0,0 +1,76 @@ +""" +Tests of execution of mul operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "function", + [ + pytest.param( + lambda x: x * 3, + id="x * 3", + ), + pytest.param( + lambda x: 3 * x, + id="3 * x", + ), + pytest.param( + lambda x: np.dot(x, 3), + id="np.dot(x, 3)", + ), + pytest.param( + lambda x: np.dot(3, x), + id="np.dot(3, x)", + ), + pytest.param( + lambda x: x * np.array([1, 2, 3]), + id="x * [1, 2, 3]", + ), + pytest.param( + lambda x: np.array([1, 2, 3]) * x, + id="[1, 2, 3] * x", + ), + pytest.param( + lambda x: x * np.array([[1, 2, 3], [3, 1, 2]]), + id="x * [[1, 2, 3], [3, 1, 2]]", + ), + pytest.param( + lambda x: np.array([[1, 2, 3], [3, 1, 2]]) * x, + id="[[1, 2, 3], [3, 1, 2]] * x", + ), + ], +) +@pytest.mark.parametrize( + "parameters", + [ + { + "x": {"range": [0, 40], "status": "encrypted"}, + }, + { + "x": {"range": [0, 40], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 40], "status": "encrypted", "shape": (2, 3)}, + }, + ], +) +def test_constant_mul(function, parameters, helpers): + """ + Test mul where one of the operators is a constant. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample) diff --git a/tests/execution/test_neg.py b/tests/execution/test_neg.py new file mode 100644 index 000000000..81af6fa3b --- /dev/null +++ b/tests/execution/test_neg.py @@ -0,0 +1,46 @@ +""" +Tests of execution of neg operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "parameters", + [ + { + "x": {"range": [0, 64], "status": "encrypted"}, + }, + { + "x": {"range": [0, 64], "status": "encrypted", "shape": (3, 2)}, + }, + ], +) +def test_neg(parameters, helpers): + """ + Test neg. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + @cnp.compiler(parameter_encryption_statuses, configuration=configuration) + def operator(x): + return -x + + @cnp.compiler(parameter_encryption_statuses, configuration=configuration) + def function(x): + return np.negative(x) + + inputset = helpers.generate_inputset(parameters) + + operator_circuit = operator.compile(inputset) + function_circuit = function.compile(inputset) + + sample = helpers.generate_sample(parameters) + + helpers.check_execution(operator_circuit, operator, sample) + helpers.check_execution(function_circuit, function, sample) diff --git a/tests/execution/test_others.py b/tests/execution/test_others.py new file mode 100644 index 000000000..07e5b98e2 --- /dev/null +++ b/tests/execution/test_others.py @@ -0,0 +1,589 @@ +""" +Tests of execution of operations converted to table lookups. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +def fusable_with_bigger_search(x, y): + """ + Fusable function that requires a single iteration for fusing. + """ + + x = x + 1 + + x_1 = x.astype(np.int32) + x_1 = x_1 + 1.5 + + x_2 = x.astype(np.int32) + x_2 = x_2 + 3.4 + + add = x_1 + x_2 + add_int = add.astype(np.int32) + + return add_int + y + + +def fusable_with_bigger_search_needs_second_iteration(x, y): + """ + Fusable function that requires more than one iteration for fusing. + """ + + x = x + 1 + x = x + 0.5 + x = np.cos(x) + + x_1 = x.astype(np.int32) + x_1 = x_1 + 1.5 + + x_p = x + 1 + x_p2 = x_p + 1 + + x_2 = (x_p + x_p2).astype(np.int32) + x_2 = x_2 + 3.4 + + add = x_1 + x_2 + add_int = add.astype(np.int32) + + return add_int + y + + +def fusable_with_one_of_the_start_nodes_is_lca_generator(): + """ + Generator of a fusable function that has one of the start nodes as lca. + """ + + # pylint: disable=invalid-name,too-many-locals,too-many-statements + + def subgraph_18(x): + t0 = 0 + t1 = 3 + t2 = 2 + t3 = 2.4688520431518555 + t4 = 2.4688520431518555 + t5 = x + t6 = np.multiply(t4, t5) + t7 = np.true_divide(t6, t3) + t8 = np.add(t7, t2) + t9 = np.rint(t8) + t10 = np.clip(t9, t0, t1) + t11 = t10.astype(np.int32) + return t11 + + def subgraph_24(x): + t0 = 0 + t1 = [0.15588106, -0.01305565] + t2 = 1.3664466152828822 + t3 = [[4, -4]] + t4 = 0 + t5 = x + t6 = t5.astype(np.float32) + t7 = np.add(t6, t4) + t8 = np.add(t7, t3) + t9 = np.multiply(t2, t8) + t10 = np.add(t1, t9) + t11 = np.greater(t10, t0) + return t11 + + cst0 = np.random.randint(-2, 2, size=(10, 2)) + cst1 = np.random.randint(0, 2, size=(10, 1)) + + def function(x): + t0 = 0 + t1 = 3 + t2 = 1 + t3 = 1.2921873902965313 + t4 = 1.0507009873554805 + t5 = 1 + t6 = 1.7580993408473766 + t7 = [0.15588106, -0.01305565] + t8 = 1 + t9 = 1.3664466152828822 + t10 = [[4, -4]] + t11 = 0 + t12 = cst0 + t13 = 0 + t14 = cst1 + t15 = x + t16 = -2 + t17 = np.add(t15, t16) + t18 = subgraph_18(t17) + t19 = np.matmul(t18, t12) + t20 = np.matmul(t18, t14) + t21 = np.multiply(t13, t20) + t22 = np.add(t19, t21) + t23 = t22.astype(np.float32) + t24 = subgraph_24(t22) + t25 = np.add(t23, t11) + t26 = np.subtract(t5, t24) + t27 = np.add(t25, t10) + t28 = np.multiply(t9, t27) + t29 = np.add(t7, t28) + t30 = np.multiply(t4, t29) + t31 = np.exp(t29) + t32 = np.multiply(t24, t30) + t33 = np.subtract(t31, t8) + t34 = np.multiply(t6, t33) + t35 = np.multiply(t26, t34) + t36 = np.add(t32, t35) + t37 = np.true_divide(t36, t3) + t38 = np.add(t37, t2) + t39 = np.rint(t38) + t40 = np.clip(t39, t0, t1) + t41 = t40.astype(np.int32) + return t41 + + return function + + # pylint: enable=invalid-name,too-many-locals,too-many-statements + + +@pytest.mark.parametrize( + "function,parameters", + [ + pytest.param( + lambda x: x // 3, + { + "x": {"status": "encrypted", "range": [0, 127]}, + }, + id="x // 3", + ), + pytest.param( + lambda x: 127 // x, + { + "x": {"status": "encrypted", "range": [1, 127]}, + }, + id="127 // x", + ), + pytest.param( + lambda x: (x / 3).astype(np.uint8), + { + "x": {"status": "encrypted", "range": [0, 127]}, + }, + id="(x / 3).astype(np.uint8)", + ), + pytest.param( + lambda x: (127 / x).astype(np.uint8), + { + "x": {"status": "encrypted", "range": [1, 127]}, + }, + id="(127 / x).astype(np.uint8)", + ), + pytest.param( + lambda x: x ** 2, + { + "x": {"status": "encrypted", "range": [0, 11]}, + }, + id="x ** 2", + ), + pytest.param( + lambda x: 2 ** x, + { + "x": {"status": "encrypted", "range": [0, 6]}, + }, + id="2 ** x", + ), + pytest.param( + lambda x: x % 10, + { + "x": {"status": "encrypted", "range": [0, 127]}, + }, + id="x % 10", + ), + pytest.param( + lambda x: 121 % x, + { + "x": {"status": "encrypted", "range": [1, 127]}, + }, + id="121 % x", + ), + pytest.param( + lambda x: +x, + { + "x": {"status": "encrypted", "range": [0, 127]}, + }, + id="+x", + ), + pytest.param( + lambda x: abs(42 - x), + { + "x": {"status": "encrypted", "range": [0, 84]}, + }, + id="abs(64 - x)", + ), + pytest.param( + lambda x: ~x, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="~x", + ), + pytest.param( + lambda x: x & 10, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="x & 10", + ), + pytest.param( + lambda x: 5 & x, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="5 & x", + ), + pytest.param( + lambda x: x | 6, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="x | 6", + ), + pytest.param( + lambda x: 11 | x, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="11 | x", + ), + pytest.param( + lambda x: x ^ 9, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="x ^ 9", + ), + pytest.param( + lambda x: 13 ^ x, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="13 ^ x", + ), + pytest.param( + lambda x: x << 2, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="x << 2", + ), + pytest.param( + lambda x: 2 << x, + { + "x": {"status": "encrypted", "range": [0, 5]}, + }, + id="2 << x", + ), + pytest.param( + lambda x: x >> 2, + { + "x": {"status": "encrypted", "range": [0, 120]}, + }, + id="x >> 2", + ), + pytest.param( + lambda x: 120 >> x, + { + "x": {"status": "encrypted", "range": [0, 16]}, + }, + id="120 >> x", + ), + pytest.param( + lambda x: x > 50, + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="x > 50", + ), + pytest.param( + lambda x: 50 > x, # pylint: disable=misplaced-comparison-constant + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="50 > x", + ), + pytest.param( + lambda x: x < 50, + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="x < 50", + ), + pytest.param( + lambda x: 50 < x, # pylint: disable=misplaced-comparison-constant + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="50 < x", + ), + pytest.param( + lambda x: x >= 50, + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="x >= 50", + ), + pytest.param( + lambda x: 50 >= x, # pylint: disable=misplaced-comparison-constant + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="50 >= x", + ), + pytest.param( + lambda x: x <= 50, + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="x <= 50", + ), + pytest.param( + lambda x: 50 <= x, # pylint: disable=misplaced-comparison-constant + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="50 <= x", + ), + pytest.param( + lambda x: x == 50, + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="x == 50", + ), + pytest.param( + lambda x: 50 == x, # pylint: disable=misplaced-comparison-constant + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="50 == x", + ), + pytest.param( + lambda x: x != 50, + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="x != 50", + ), + pytest.param( + lambda x: 50 != x, # pylint: disable=misplaced-comparison-constant + { + "x": {"status": "encrypted", "range": [0, 100]}, + }, + id="50 != x", + ), + pytest.param( + lambda x: x.clip(5, 10), + { + "x": {"status": "encrypted", "range": [0, 15]}, + }, + id="x.clip(5, 10)", + ), + pytest.param( + lambda x: (60 * np.sin(x)).astype(np.int8) + 60, + { + "x": {"status": "encrypted", "range": [0, 127]}, + }, + id="(60 * np.sin(x)).astype(np.int8) + 60", + ), + pytest.param( + lambda x: ((np.sin(x) ** 2) + (np.cos(x) ** 2)).astype(np.uint8), + { + "x": {"status": "encrypted", "range": [0, 127]}, + }, + id="((np.sin(x) ** 2) + (np.cos(x) ** 2)).astype(np.uint8)", + ), + pytest.param( + lambda x: np.maximum(x, [[10, 20], [30, 40], [50, 60]]), + { + "x": {"status": "encrypted", "range": [0, 127], "shape": (3, 2)}, + }, + id="np.maximum(x, [[10, 20], [30, 40], [50, 60]])", + ), + pytest.param( + fusable_with_bigger_search, + { + "x": {"status": "encrypted", "range": [5, 10]}, + "y": {"status": "encrypted", "range": [5, 10]}, + }, + id="fusable_with_bigger_search", + ), + pytest.param( + fusable_with_bigger_search_needs_second_iteration, + { + "x": {"status": "encrypted", "range": [5, 10]}, + "y": {"status": "encrypted", "range": [5, 10]}, + }, + id="fusable_with_bigger_search_needs_second_iteration", + ), + pytest.param( + fusable_with_one_of_the_start_nodes_is_lca_generator(), + { + "x": {"status": "encrypted", "range": [0, 4], "shape": (1, 10)}, + }, + id="fusable_with_one_of_the_start_nodes_is_lca", + ), + pytest.param( + lambda x: x + x.shape[0] + x.ndim + x.size, + { + "x": {"status": "encrypted", "range": [0, 15], "shape": (3, 2)}, + }, + id="x + shape[0] + x.ndim + x.size", + ), + ], +) +def test_others(function, parameters, helpers): + """ + Test others. + """ + + # scalar + # ------ + + if "shape" not in parameters["x"]: + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample, retries=10) + + # tensor + # ------ + + if "shape" not in parameters["x"]: + parameters["x"]["shape"] = (3, 2) + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample, retries=10) + + +def test_others_bad_fusing(helpers): + """ + Test others with bad fusing. + """ + + configuration = helpers.configuration() + + # two variable inputs + # ------------------- + + @cnp.compiler({"x": "encrypted", "y": "clear"}, configuration=configuration) + def function1(x, y): + return (10 * (np.sin(x) ** 2) + 10 * (np.cos(y) ** 2)).astype(np.uint8) + + with pytest.raises(RuntimeError) as excinfo: + inputset = [(i, i) for i in range(100)] + function1.compile(inputset) + + helpers.check_str( + # pylint: disable=line-too-long + """ + +Function you are trying to compile cannot be converted to MLIR + + %0 = 10 # ClearScalar + %1 = 10 # ClearScalar + %2 = 2 # ClearScalar + %3 = 2 # ClearScalar + %4 = x # EncryptedScalar + %5 = y # ClearScalar + %6 = sin(%4) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported + %7 = cos(%5) # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported + %8 = power(%6, %2) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported + %9 = power(%7, %3) # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%10 = multiply(%0, %8) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%11 = multiply(%1, %9) # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%12 = add(%10, %11) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%13 = astype(%12, dtype=ubyte) # EncryptedScalar +return %13 + + """, # noqa: E501 + # pylint: enable=line-too-long + str(excinfo.value), + ) + + # big intermediate constants + # -------------------------- + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def function2(x): + return (np.sin(x) * [[1, 2], [3, 4]]).astype(np.int8) + + with pytest.raises(RuntimeError) as excinfo: + inputset = range(100) + function2.compile(inputset) + + helpers.check_str( + # pylint: disable=line-too-long + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = [[1 2] [3 4]] # ClearTensor +%1 = x # EncryptedScalar +%2 = sin(%1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%3 = multiply(%2, %0) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%4 = astype(%3, dtype=byte) # EncryptedTensor +return %4 + + """, # noqa: E501 + # pylint: enable=line-too-long + str(excinfo.value), + ) + + # intermediates with different shape + # ---------------------------------- + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def function3(x): + return np.abs(np.sin(x)).reshape((2, 3)).astype(np.uint8) + + with pytest.raises(RuntimeError) as excinfo: + inputset = [np.random.randint(0, 2 ** 7, size=(3, 2)) for _ in range(100)] + function3.compile(inputset) + + helpers.check_str( + # pylint: disable=line-too-long + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedTensor +%1 = sin(%0) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%2 = absolute(%1) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%3 = reshape(%2, newshape=(2, 3)) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +%4 = astype(%3, dtype=ubyte) # EncryptedTensor +return %4 + + """, # noqa: E501 + # pylint: enable=line-too-long + str(excinfo.value), + ) diff --git a/tests/execution/test_reshape.py b/tests/execution/test_reshape.py new file mode 100644 index 000000000..ac4b176d6 --- /dev/null +++ b/tests/execution/test_reshape.py @@ -0,0 +1,170 @@ +""" +Tests of execution of reshape operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "shape,newshape", + [ + pytest.param( + (12,), + (12, 1), + ), + pytest.param( + (12,), + (1, 12), + ), + pytest.param( + (12,), + (3, 4), + ), + pytest.param( + (12,), + (3, 2, 2), + ), + pytest.param( + (3, 4), + 12, + ), + pytest.param( + (3, 4), + (12,), + ), + pytest.param( + (3, 4), + (4, 3), + ), + pytest.param( + (3, 4), + (2, 2, 3), + ), + pytest.param( + (3, 4), + (2, 3, 2), + ), + pytest.param( + (3, 4), + (3, 2, 2), + ), + pytest.param( + (3, 4), + (3, 1, 4), + ), + pytest.param( + (3, 4), + (12, 1), + ), + pytest.param( + (3, 4), + (-1,), + ), + pytest.param( + (3, 4), + -1, + ), + pytest.param( + (2, 2, 3), + (3, 4), + ), + pytest.param( + (2, 2, 3), + (4, 3), + ), + pytest.param( + (2, 2, 3), + (3, 2, 2), + ), + pytest.param( + (2, 3, 4, 5, 6), + (6, 4, 30), + ), + pytest.param( + (6, 4, 30), + (2, 3, 4, 5, 6), + ), + pytest.param( + (2, 3, 4, 5, 6), + (2, 60, 6), + ), + pytest.param( + (2, 60, 6), + (2, 3, 4, 5, 6), + ), + pytest.param( + (2, 3, 2, 3, 4), + (6, 6, -1), + ), + pytest.param( + (2, 3, 2, 3, 4), + (6, -1, 12), + ), + pytest.param( + (2, 3, 2, 3, 4), + (-1, 18, 4), + ), + ], +) +def test_reshape(shape, newshape, helpers): + """ + Test reshape. + """ + + configuration = helpers.configuration() + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def function(x): + return np.reshape(x, newshape) + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def method(x): + return x.reshape(newshape) + + inputset = [np.random.randint(0, 2 ** 5, size=shape) for i in range(100)] + + function_circuit = function.compile(inputset) + method_circuit = method.compile(inputset) + + sample = np.random.randint(0, 2 ** 5, size=shape, dtype=np.uint8) + + helpers.check_execution(function_circuit, function, sample) + helpers.check_execution(method_circuit, method, sample) + + +@pytest.mark.parametrize( + "shape", + [ + pytest.param( + (12,), + ), + pytest.param( + (3, 4), + ), + pytest.param( + (2, 2, 3), + ), + pytest.param( + (2, 3, 4, 5, 6), + ), + ], +) +def test_flatten(shape, helpers): + """ + Test flatten. + """ + + configuration = helpers.configuration() + + @cnp.compiler({"x": "encrypted"}, configuration=configuration) + def function(x): + return x.flatten() + + inputset = [np.random.randint(0, 2 ** 5, size=shape) for i in range(100)] + circuit = function.compile(inputset) + + sample = np.random.randint(0, 2 ** 5, size=shape, dtype=np.uint8) + helpers.check_execution(circuit, function, sample) diff --git a/tests/execution/test_static_indexing.py b/tests/execution/test_static_indexing.py new file mode 100644 index 000000000..032251bc6 --- /dev/null +++ b/tests/execution/test_static_indexing.py @@ -0,0 +1,193 @@ +""" +Tests of executiwhere x.shape == of static indexing operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "shape,function", + [ + pytest.param( + (3,), + lambda x: x[0], + id="x[0] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[1], + id="x[1] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[2], + id="x[2] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[-1], + id="x[-1] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[-2], + id="x[-2] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[-3], + id="x[-3] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[0:1], + id="x[0:1] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[1:2], + id="x[1:2] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[2:3], + id="x[2:3] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[0:2], + id="x[0:2] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[1:3], + id="x[1:3] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[0:3], + id="x[0:3] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[2:0:-1], + id="x[2:0:-1] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[::-1], + id="x[::-1] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[:-1], + id="x[:-1] where x.shape == (3,)", + ), + pytest.param( + (3,), + lambda x: x[-2:], + id="x[-2:] where x.shape == (3,)", + ), + pytest.param( + (3, 4), + lambda x: x[0, 0], + id="x[0, 0] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[0, -1], + id="x[0, -1] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[-1, 0], + id="x[-1, 0] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[-1, -1], + id="x[-1, -1] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[2, 1], + id="x[2, 1] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[0], + id="x[0] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[:, 0], + id="x[:, 0] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[-1], + id="x[-1] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[:, -1], + id="x[:, -1] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[1:3, 1:3], + id="x[1:3, 1:3] where x.shape == (3, 4)", + ), + pytest.param( + (3, 4), + lambda x: x[::-1], + id="x[::-1] where x.shape == (3, 4)", + ), + ], +) +def test_static_indexing(shape, function, helpers): + """ + Test static indexing. + """ + + configuration = helpers.configuration() + compiler = cnp.Compiler(function, {"x": "encrypted"}, configuration) + + inputset = [np.random.randint(0, 2 ** 5, size=shape) for _ in range(100)] + circuit = compiler.compile(inputset) + + sample = np.random.randint(0, 2 ** 5, size=shape, dtype=np.uint8) + helpers.check_execution(circuit, function, sample) + + +def test_bad_static_indexing(helpers): + """ + Test static indexing with bad parameters. + """ + + configuration = helpers.configuration() + + # with float + # ---------- + + compiler = cnp.Compiler(lambda x: x[1.5], {"x": "encrypted"}, configuration) + + inputset = [np.random.randint(0, 2 ** 3, size=(3,)) for _ in range(100)] + with pytest.raises(ValueError) as excinfo: + compiler.compile(inputset) + + assert str(excinfo.value) == "Indexing with '1.5' is not supported" + + # with bad slice + # -------------- + + compiler = cnp.Compiler(lambda x: x[slice(1.5, 2.5, None)], {"x": "encrypted"}, configuration) + + inputset = [np.random.randint(0, 2 ** 3, size=(3,)) for _ in range(100)] + with pytest.raises(ValueError) as excinfo: + compiler.compile(inputset) + + assert str(excinfo.value) == "Indexing with '1.5:2.5' is not supported" diff --git a/tests/execution/test_sub.py b/tests/execution/test_sub.py new file mode 100644 index 000000000..d94e21bb1 --- /dev/null +++ b/tests/execution/test_sub.py @@ -0,0 +1,56 @@ +""" +Tests of execution of sub operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "function", + [ + pytest.param( + lambda x: 42 - x, + id="42 - x", + ), + pytest.param( + lambda x: np.array([1, 2, 3]) - x, + id="[1, 2, 3] - x", + ), + pytest.param( + lambda x: np.array([[1, 2, 3], [4, 5, 6]]) - x, + id="[[1, 2, 3], [4, 5, 6]] - x", + ), + ], +) +@pytest.mark.parametrize( + "parameters", + [ + { + "x": {"range": [0, 60], "status": "encrypted"}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (3,)}, + }, + { + "x": {"range": [0, 60], "status": "encrypted", "shape": (2, 3)}, + }, + ], +) +def test_constant_sub(function, parameters, helpers): + """ + Test sub where one of the operators is a constant. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample) diff --git a/tests/execution/test_sum.py b/tests/execution/test_sum.py new file mode 100644 index 000000000..2a9747b8d --- /dev/null +++ b/tests/execution/test_sum.py @@ -0,0 +1,114 @@ +""" +Tests of execution of sum operation. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "function,parameters", + [ + pytest.param( + lambda x: np.sum(x), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=0), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=1), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=-1), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=-2), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=(0, 1)), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=(-2, -1)), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=0, keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=1, keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=-1, keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=-2, keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=(0, 1), keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + pytest.param( + lambda x: np.sum(x, axis=(-2, -1), keepdims=True), + { + "x": {"shape": (3, 2), "range": [0, 10], "status": "encrypted"}, + }, + ), + ], +) +def test_sum(function, parameters, helpers): + """ + Test sum. + """ + + parameter_encryption_statuses = helpers.generate_encryption_statuses(parameters) + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, parameter_encryption_statuses, configuration) + + inputset = helpers.generate_inputset(parameters) + circuit = compiler.compile(inputset) + + sample = helpers.generate_sample(parameters) + helpers.check_execution(circuit, function, sample) diff --git a/tests/internal/__init__.py b/tests/internal/__init__.py new file mode 100644 index 000000000..4a8830f30 --- /dev/null +++ b/tests/internal/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of `concrete.numpy.internal` namespace. +""" diff --git a/tests/internal/test_utils.py b/tests/internal/test_utils.py new file mode 100644 index 000000000..57a9fd7de --- /dev/null +++ b/tests/internal/test_utils.py @@ -0,0 +1,29 @@ +""" +Tests of utilities related to the entire project. +""" + +import pytest + +from concrete.numpy.internal.utils import assert_that, unreachable + + +def test_assert_that(): + """ + Test `assert_that` function. + """ + + with pytest.raises(AssertionError) as excinfo: + assert_that(2 + 2 == 3, "no") + + assert str(excinfo.value) == "no" + + +def test_unreachable(): + """ + Test `unreachable` function. + """ + + with pytest.raises(RuntimeError) as excinfo: + unreachable() + + assert str(excinfo.value) == "Entered unreachable code" diff --git a/tests/mlir/__init__.py b/tests/mlir/__init__.py new file mode 100644 index 000000000..dcfe544c8 --- /dev/null +++ b/tests/mlir/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of `concrete.numpy.mlir` namespace. +""" diff --git a/tests/mlir/test_graph_converter.py b/tests/mlir/test_graph_converter.py new file mode 100644 index 000000000..5983e8196 --- /dev/null +++ b/tests/mlir/test_graph_converter.py @@ -0,0 +1,337 @@ +""" +Tests of `GraphConverter` class. +""" + +import numpy as np +import pytest + +import concrete.numpy as cnp + +# pylint: disable=line-too-long + + +@pytest.mark.parametrize( + "function,encryption_statuses,inputset,expected_error,expected_message", + [ + pytest.param( + lambda x, y: (x - y, x + y), + {"x": "encrypted", "y": "clear"}, + [(np.random.randint(0, 2 ** 3), np.random.randint(0, 2 ** 3)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedScalar +%1 = y # ClearScalar +%2 = subtract(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only a single output is supported +%3 = add(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only a single output is supported +return (%2, %3) + + """, # noqa: E501 + ), + pytest.param( + lambda x: x * 1.5, + {"x": "encrypted"}, + [2.5 * x for x in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only unsigned integer inputs are supported +%1 = 1.5 # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer constants are supported +%2 = multiply(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x: np.sin(x), + {"x": "encrypted"}, + range(100), + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedScalar +%1 = sin(%0) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only integer operations are supported +return %1 + + """, # noqa: E501 + ), + pytest.param( + lambda x, y: np.concatenate((x, y)), + {"x": "encrypted", "y": "clear"}, + [ + ( + np.random.randint(0, 2 ** 3, size=(3, 2)), + np.random.randint(0, 2 ** 3, size=(3, 2)), + ) + for _ in range(100) + ], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedTensor +%1 = y # ClearTensor +%2 = concatenate((%0, %1)) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only all encrypted concatenate is supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x, w: cnp.conv2d(x, w), + {"x": "encrypted", "w": "encrypted"}, + [ + ( + np.random.randint(0, 2, size=(1, 1, 4, 4)), + np.random.randint(0, 2, size=(1, 1, 1, 1)), + ) + for _ in range(100) + ], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedTensor +%1 = w # EncryptedTensor +%2 = conv2d(%0, %1, [0], pads=(0, 0, 0, 0), strides=(1, 1), dilations=(1, 1)) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only conv2d with encrypted input and clear weight is supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x, y: np.dot(x, y), + {"x": "encrypted", "y": "encrypted"}, + [ + ( + np.random.randint(0, 2 ** 2, size=(1,)), + np.random.randint(0, 2 ** 2, size=(1,)), + ) + for _ in range(100) + ], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedTensor +%1 = y # EncryptedTensor +%2 = dot(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only dot product between encrypted and clear is supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x: x[0], + {"x": "clear"}, + [np.random.randint(0, 2 ** 3, size=(4,)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # ClearTensor +%1 = %0[0] # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only encrypted indexing supported +return %1 + + """, # noqa: E501 + ), + pytest.param( + lambda x, y: x @ y, + {"x": "encrypted", "y": "encrypted"}, + [ + ( + np.random.randint(0, 2 ** 2, size=(1, 1)), + np.random.randint(0, 2 ** 2, size=(1, 1)), + ) + for _ in range(100) + ], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedTensor +%1 = y # EncryptedTensor +%2 = matmul(%0, %1) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only matrix multiplication between encrypted and clear is supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x, y: x * y, + {"x": "encrypted", "y": "encrypted"}, + [(np.random.randint(0, 2 ** 3), np.random.randint(0, 2 ** 3)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedScalar +%1 = y # EncryptedScalar +%2 = multiply(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only multiplication between encrypted and clear is supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x: -x, + {"x": "clear"}, + [np.random.randint(0, 2 ** 3) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # ClearScalar +%1 = negative(%0) # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only encrypted negation is supported +return %1 + + """, # noqa: E501 + ), + pytest.param( + lambda x: x.reshape((3, 2)), + {"x": "clear"}, + [np.random.randint(0, 2 ** 3, size=(2, 3)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # ClearTensor +%1 = reshape(%0, newshape=(3, 2)) # ClearTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only encrypted reshape is supported +return %1 + + """, # noqa: E501 + ), + pytest.param( + lambda x: x - 1, + {"x": "clear"}, + [np.random.randint(0, 2 ** 3, size=(2, 3)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # ClearTensor +%1 = 1 # ClearScalar +%2 = subtract(%0, %1) # ClearTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only subtraction of encrypted from clear is supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x: np.sum(x), + {"x": "clear"}, + [np.random.randint(0, 2, size=(1,)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # ClearTensor +%1 = sum(%0) # ClearScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only encrypted sum is supported +return %1 + + """, # noqa: E501 + ), + pytest.param( + lambda x, y: np.maximum(x, y), + {"x": "encrypted", "y": "clear"}, + [ + (np.random.randint(0, 2, size=(1,)), np.random.randint(0, 2, size=(1,))) + for _ in range(100) + ], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # EncryptedTensor +%1 = y # ClearTensor +%2 = maximum(%0, %1) # EncryptedTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only single input table lookups are supported +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x: np.maximum(x, np.array([3])), + {"x": "clear"}, + [np.random.randint(0, 2, size=(1,)) for _ in range(100)], + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR + +%0 = x # ClearTensor +%1 = [3] # ClearTensor +%2 = maximum(%0, %1) # ClearTensor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one of the operands must be encrypted +return %2 + + """, # noqa: E501 + ), + pytest.param( + lambda x: x + 200, + {"x": "encrypted"}, + range(200), + RuntimeError, + """ + +Function you are trying to compile cannot be converted to MLIR: + +%0 = x # EncryptedScalar +%1 = 200 # ClearScalar +%2 = add(%0, %1) # EncryptedScalar +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only up to 8-bit integers are supported +return %2 + + """, # noqa: E501 + ), + ], +) +def test_graph_converter_bad_convert( + function, + encryption_statuses, + inputset, + expected_error, + expected_message, + helpers, +): + """ + Test unsupported graph conversion. + """ + + configuration = helpers.configuration() + compiler = cnp.Compiler(function, encryption_statuses, configuration) + + with pytest.raises(expected_error) as excinfo: + compiler.compile(inputset) + + helpers.check_str(expected_message, str(excinfo.value)) + + +# pylint: enable=line-too-long diff --git a/tests/representation/__init__.py b/tests/representation/__init__.py new file mode 100644 index 000000000..0210d8d50 --- /dev/null +++ b/tests/representation/__init__.py @@ -0,0 +1,3 @@ +""" +Tests of `concrete.numpy.representation` namespace. +""" diff --git a/tests/representation/test_graph.py b/tests/representation/test_graph.py new file mode 100644 index 000000000..b8e80ead0 --- /dev/null +++ b/tests/representation/test_graph.py @@ -0,0 +1,47 @@ +""" +Tests of `Graph` class. +""" + +import pytest + +import concrete.numpy as cnp + + +@pytest.mark.parametrize( + "function,inputset,expected_result", + [ + pytest.param( + lambda x: x + 1, + range(5), + 3, + ), + pytest.param( + lambda x: x + 42, + range(10), + 6, + ), + pytest.param( + lambda x: x + 42, + range(50), + 7, + ), + pytest.param( + lambda x: x + 1.2, + [1.5, 4.2], + -1, + ), + ], +) +def test_graph_maximum_integer_bit_width(function, inputset, expected_result, helpers): + """ + Test `maximum_integer_bit_width` method of `Graph` class. + """ + + configuration = helpers.configuration() + + compiler = cnp.Compiler(function, {"x": "encrypted"}, configuration=configuration) + graph = compiler.trace(inputset) + + print(graph.format()) + + assert graph.maximum_integer_bit_width() == expected_result diff --git a/tests/representation/test_node.py b/tests/representation/test_node.py new file mode 100644 index 000000000..550236cf4 --- /dev/null +++ b/tests/representation/test_node.py @@ -0,0 +1,237 @@ +""" +Tests of `Node` class. +""" + +import numpy as np +import pytest + +from concrete.numpy.dtypes import UnsignedInteger +from concrete.numpy.representation import Node +from concrete.numpy.values import EncryptedScalar, EncryptedTensor, Value + + +@pytest.mark.parametrize( + "constant,expected_error,expected_message", + [ + pytest.param( + "abc", + ValueError, + "Constant 'abc' is not supported", + ), + ], +) +def test_node_bad_constant(constant, expected_error, expected_message): + """ + Test `constant` function of `Node` class with bad parameters. + """ + + with pytest.raises(expected_error) as excinfo: + Node.constant(constant) + + assert str(excinfo.value) == expected_message + + +@pytest.mark.parametrize( + "node,args,expected_error,expected_message", + [ + pytest.param( + Node.constant(1), + ["abc"], + ValueError, + "Evaluation of constant '1' node using 'abc' failed " + "because of invalid number of arguments", + ), + pytest.param( + Node.generic( + name="add", + inputs=[Value.of(4), Value.of(10, is_encrypted=True)], + output=Value.of(14), + operation=lambda x, y: x + y, + ), + ["abc"], + ValueError, + "Evaluation of generic 'add' node using 'abc' failed " + "because of invalid number of arguments", + ), + pytest.param( + Node.generic( + name="add", + inputs=[Value.of(4), Value.of(10, is_encrypted=True)], + output=Value.of(14), + operation=lambda x, y: x + y, + ), + ["abc", "def"], + ValueError, + "Evaluation of generic 'add' node using 'abc', 'def' failed " + "because argument 'abc' is not valid", + ), + pytest.param( + Node.generic( + name="add", + inputs=[Value.of([3, 4]), Value.of(10, is_encrypted=True)], + output=Value.of([13, 14]), + operation=lambda x, y: x + y, + ), + [[1, 2, 3, 4], 10], + ValueError, + "Evaluation of generic 'add' node using [1, 2, 3, 4], 10 failed " + "because argument [1, 2, 3, 4] does not have the expected shape of (2,)", + ), + pytest.param( + Node.generic( + name="unknown", + inputs=[], + output=Value.of(10), + operation=lambda: "abc", + ), + [], + ValueError, + "Evaluation of generic 'unknown' node resulted in 'abc' of type str " + "which is not acceptable either because of the type or because of overflow", + ), + pytest.param( + Node.generic( + name="unknown", + inputs=[], + output=Value.of(10), + operation=lambda: np.array(["abc", "def"]), + ), + [], + ValueError, + "Evaluation of generic 'unknown' node resulted in array(['abc', 'def'], dtype='", + ), + pytest.param( + EncryptedScalar(SignedInteger(5)), + "EncryptedScalar", + ), + pytest.param( + ClearTensor(SignedInteger(5), shape=(3, 2)), + "ClearTensor", + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(3, 2)), + "EncryptedTensor", + ), + ], +) +def test_value_str(data_type, expected_result): + """ + Test `__str__` method of `Value` class. + """ + + assert str(data_type) == expected_result + + +@pytest.mark.parametrize( + "value,expected_result", + [ + pytest.param( + ClearScalar(SignedInteger(5)), + True, + ), + pytest.param( + EncryptedScalar(SignedInteger(5)), + False, + ), + ], +) +def test_value_is_clear(value, expected_result): + """ + Test `is_clear` property of `Value` class. + """ + + assert value.is_clear == expected_result + + +@pytest.mark.parametrize( + "value,expected_result", + [ + pytest.param( + EncryptedScalar(SignedInteger(5)), + True, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(3, 2)), + False, + ), + ], +) +def test_value_is_scalar(value, expected_result): + """ + Test `is_scalar` property of `Value` class. + """ + + assert value.is_scalar == expected_result + + +@pytest.mark.parametrize( + "value,expected_result", + [ + pytest.param( + EncryptedScalar(SignedInteger(5)), + 0, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(3,)), + 1, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(3, 2)), + 2, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(5, 3, 2)), + 3, + ), + ], +) +def test_value_ndim(value, expected_result): + """ + Test `ndim` property of `Value` class. + """ + + assert value.ndim == expected_result + + +@pytest.mark.parametrize( + "value,expected_result", + [ + pytest.param( + EncryptedScalar(SignedInteger(5)), + 1, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(3,)), + 3, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(3, 2)), + 6, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(5, 3, 2)), + 30, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(1,)), + 1, + ), + pytest.param( + EncryptedTensor(SignedInteger(5), shape=(1, 1)), + 1, + ), + ], +) +def test_value_size(value, expected_result): + """ + Test `size` property of `Value` class. + """ + + assert value.size == expected_result