diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml new file mode 100644 index 0000000..2f483b7 --- /dev/null +++ b/.github/workflows/publish_to_pypi.yml @@ -0,0 +1,41 @@ +name: Publish Python ๐Ÿ distribution ๐Ÿ“ฆ to PyPI + +on: push + +jobs: + build: + name: Build distribution ๐Ÿ“ฆ + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v3 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: >- + Publish Python ๐Ÿ distribution ๐Ÿ“ฆ to PyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/oraqle + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..0110efc --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff pytest + pip install -r requirements.txt + pip install -e . + - name: Lint with ruff + run: | + ruff check + - name: Test with pytest + run: | + pytest diff --git a/.gitignore b/.gitignore index f9606a3..c8cf315 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,13 @@ /venv +*.dot +.idea/** +__pycache__/** +/build +.DS_Store +*.egg-info* +instructions.txt +.ipynb_checkpoints/ +*.pdf +*.pkl +.sphinx_build/ +/dist diff --git a/README.md b/README.md index 1ef3cd1..a88d6f8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # Oraqle -The first version of the oraqle compiler will be released in April 2024. +The oraqle compiler lets you generate arithmetic circuits from high-level Python code. It also lets you generate code using HElib. + +This repository uses a fork of fhegen as a dependency and adapts some of the code from [fhegen](https://github.com/Crypto-TII/fhegen), which was written by Johannes Mono, Chiara Marcolla, Georg Land, Tim Gรผneysu, and Najwa Aaraj. You can read their theoretical work at: https://eprint.iacr.org/2022/706. + +## Setting up +The best way to get things up and running is using a virtual environment: +- Set up a virtualenv using `python3 -m venv venv` in the directory. +- Enter the virtual environment using `source venv/bin/activate`. +- Install the requirements using `pip install requirements.txt`. +- *To overcome import problems*, run `pip install -e .`, which will create links to your files (so you do not need to re-install after every change). diff --git a/addchain_cache.db b/addchain_cache.db new file mode 100644 index 0000000..e211620 Binary files /dev/null and b/addchain_cache.db differ diff --git a/compiler/hello_world.py b/compiler/hello_world.py deleted file mode 100644 index eac0111..0000000 --- a/compiler/hello_world.py +++ /dev/null @@ -1,2 +0,0 @@ -if __name__ == "__main__": - print("Hello world!") diff --git a/docs/api/abstract_nodes_api.md b/docs/api/abstract_nodes_api.md new file mode 100644 index 0000000..78621bf --- /dev/null +++ b/docs/api/abstract_nodes_api.md @@ -0,0 +1,5 @@ +# Abstract nodes API +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + +If you want to extend the oraqle compiler, or implement your own high-level nodes, it is easiest to extend one of the existing abstract node classes. diff --git a/docs/api/addition_chains_api.md b/docs/api/addition_chains_api.md new file mode 100644 index 0000000..54ef612 --- /dev/null +++ b/docs/api/addition_chains_api.md @@ -0,0 +1,11 @@ +# Addition chains API +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + +The `add_chains` module contains tools for generating addition chains. + +::: oraqle.add_chains + options: + heading_level: 2 + show_submodules: true + show_if_no_docstring: false diff --git a/docs/api/circuits_api.md b/docs/api/circuits_api.md new file mode 100644 index 0000000..e4bce56 --- /dev/null +++ b/docs/api/circuits_api.md @@ -0,0 +1,16 @@ +# Circuits API +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + + +## High-level circuits +::: oraqle.compiler.circuit.Circuit + options: + heading_level: 3 + + +## Arithmetic circuits +::: oraqle.compiler.circuit.ArithmeticCircuit + options: + heading_level: 3 + \ No newline at end of file diff --git a/docs/api/code_generation_api.md b/docs/api/code_generation_api.md new file mode 100644 index 0000000..9ef6dfe --- /dev/null +++ b/docs/api/code_generation_api.md @@ -0,0 +1,56 @@ +# Code generation API +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + +The easiest way is using: +```python3 +arithmetic_circuit.generate_code() +``` + +## Arithmetic instructions +If you want to extend the oraqle compiler, or implement your own code generation, you can use the following instructions to do so. + +??? info "Abstract instruction" + ::: oraqle.compiler.instructions.ArithmeticInstruction + options: + heading_level: 3 + +??? info "InputInstruction" + ::: oraqle.compiler.instructions.InputInstruction + options: + heading_level: 3 + +??? info "AdditionInstruction" + ::: oraqle.compiler.instructions.AdditionInstruction + options: + heading_level: 3 + +??? info "MultiplicationInstruction" + ::: oraqle.compiler.instructions.MultiplicationInstruction + options: + heading_level: 3 + +??? info "ConstantAdditionInstruction" + ::: oraqle.compiler.instructions.ConstantAdditionInstruction + options: + heading_level: 3 + +??? info "ConstantMultiplicationInstruction" + ::: oraqle.compiler.instructions.ConstantMultiplicationInstruction + options: + heading_level: 3 + +??? info "OutputInstruction" + ::: oraqle.compiler.instructions.OutputInstruction + options: + heading_level: 3 + + +## Generating arithmetic programs +::: oraqle.compiler.instructions.ArithmeticProgram + options: + heading_level: 3 + + +## Generating code for HElib +... diff --git a/docs/api/nodes_api.md b/docs/api/nodes_api.md new file mode 100644 index 0000000..e575383 --- /dev/null +++ b/docs/api/nodes_api.md @@ -0,0 +1,53 @@ +# Nodes API +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + +## Boolean operations + +??? info "AND operation" + ::: oraqle.compiler.boolean.bool_and.And + options: + heading_level: 3 + +??? info "OR operation" + ::: oraqle.compiler.boolean.bool_or.Or + options: + heading_level: 3 + +??? info "NEG operation" + ::: oraqle.compiler.boolean.bool_neg.Neg + options: + heading_level: 3 + + +## Arithmetic operations +These operations are fundamental arithmetic operations, so they will stay the same when they are arithmetized. + + +## High-level arithmetic operations + +??? info "Subtraction" + ::: oraqle.compiler.arithmetic.subtraction.Subtraction + options: + heading_level: 3 + +??? info "Exponentiation" + ::: oraqle.compiler.arithmetic.exponentiation.Power + options: + heading_level: 3 + + +## Polynomial evaluation + +??? info "Univariate polynomial evaluation" + ::: oraqle.compiler.polynomials.univariate.UnivariatePoly + options: + heading_level: 3 + + +## Control flow + +??? info "If-else statement" + ::: oraqle.compiler.control_flow.conditional.IfElse + options: + heading_level: 3 diff --git a/docs/api/pareto_fronts_api.md b/docs/api/pareto_fronts_api.md new file mode 100644 index 0000000..5376661 --- /dev/null +++ b/docs/api/pareto_fronts_api.md @@ -0,0 +1,18 @@ +# Pareto fronts API +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + +If you are using depth-aware arithmetization, you will find that the compiler does not output one arithmetic circuit. +Instead, it outputs a Pareto front, which represents the best circuits that it could generate trading off two metrics: +The *multiplicative depth* and the *multiplicative size/cost*. +This page briefly explains the API for interfacing with these Pareto fronts. + +## The abstract base class + +??? info "Abstract ParetoFront" + ::: oraqle.compiler.nodes.abstract.ParetoFront + options: + heading_level: 3 + +## Depth-size and depth-cost fronts + diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..c2c653d --- /dev/null +++ b/docs/config.md @@ -0,0 +1,7 @@ +# Configuration parameters + +::: oraqle.config + options: + heading_level: 2 + show_submodules: true + show_if_no_docstring: false diff --git a/docs/example_circuits.md b/docs/example_circuits.md new file mode 100644 index 0000000..e4eabe9 --- /dev/null +++ b/docs/example_circuits.md @@ -0,0 +1,7 @@ +!!! warning + Some of these example circuits are untested and may be incorrect. + +::: oraqle.circuits + options: + heading_level: 3 + show_submodules: true diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..a10f59e --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,85 @@ +# Getting started +In 5 minutes, this page will guide you through how to install oraqle, how to specify high-level programs, and how to arithmetize your first circuit! + +## Installation +Simply install the most recent version of the Oraqle compiler using: +``` +pip install oraqle +``` + +We use continuous integration to test every build of the Oraqle compiler on Windows, MacOS, and Unix systems. +If you do run into problems, feel free to [open an issue on GitHub]()! + +## Specifying high-level programs +Let's start with importing `galois`, which represents our plaintext algebra. +We will also immediately import the relevant oraqle classes for our little example: +```python3 +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input +``` + +For this example, we will use 31 as our plaintext modulus. This algebra is denoted by `GF(31)`. +Let's create a few inputs that represent elements in this algebra: +```python3 +gf = GF(31) + +x = Input("x", gf) +y = Input("y", gf) +z = Input("z", gf) +``` + +We can now perform some operations on these elements, and they do not have to be arithmetic operations! +For example, we can perform equality checks or comparisons: +``` +comparison = x < y +equality = y == z +both = comparison & equality +``` + +While we have specified some operations, we have not yet established this as a circuit. We will do so now: +```python3 +circuit = Circuit([both]) +``` + +And that's it! We are done specifying our first high-level circuit. +As you can see this is all very similar to writing a regular Python program. +If you want to visualize this high-level circuit before we continue with arithmetizing it, you can run the following (if you have graphviz installed): +```python3 +circuit.to_pdf("high_level_circuit.pdf") +``` + +!!! tip + If you do not have graphviz installed, you can instead call: + ```python3 + circuit.to_dot("high_level_circuit.dot") + ``` + After that, you can copy the file contents to [an online graphviz viewer](https://dreampuf.github.io/GraphvizOnline)! + +## Arithmetizing your first circuit +At this point, arithmetization is a breeze, because the oraqle compiler takes care of these steps. +We can create an arithmetic circuit and visualize it using the following snippet: +```python3 +arithmetic_circuit = circuit.arithmetize() +arithmetic_circuit.to_pdf("arithmetic_circuit.pdf") +``` + +You will notice that it's quite a large circuit. But how large is it exactly? +This is a question that we can ask to the oraqle compiler: +```python3 +print("Depth:", arithmetic_circuit.multiplicative_depth()) +print("Size:", arithmetic_circuit.multiplicative_size()) +print("Cost:", arithmetic_circuit.multiplicative_cost(0.7)) +``` + +In the last line, we asked the compiler to output the multiplicative cost, considering that squaring operations are cheaper than regular multiplications. +We weighed this cost with a factor 0.7. + +Now that we have an arithmetic circuit, we can use homomorphic encryption to evaluate it! +If you are curious about executing these circuits for real, consider reading [the code generation tutorial](tutorial_running_exps.md). + +!!! warning + There are many homomorphic encryption libraries that do not support plaintext moduli that are not NTT-friendly. The plaintext modulus we chose (31) is not NTT-friendly. + In fact, only very few primes are NTT-friendly, and they are somewhat large. This is why, right now, the oraqle compiler only implements code generation for HElib. + HElib is (as far as we are aware) the only library that supports plaintext moduli that are not NTT-friendly. diff --git a/docs/images/oraqle_logo_cropped.svg b/docs/images/oraqle_logo_cropped.svg new file mode 100644 index 0000000..f306d73 --- /dev/null +++ b/docs/images/oraqle_logo_cropped.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1dd7f78 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,18 @@ +# Welcome to oraqle +
+ Oraqle logo
+ A secure computation compiler +
+ +Simply install the most recent version of the Oraqle compiler using: +``` +pip install oraqle==0.1.0 +``` + +Consider checking out our [getting started page](getting_started.md) to help you get up to speed with arithmetizing circuits! + +## API reference +!!! warning + In this version of Oraqle, the API is still prone to changes. Paths and names can change between any version. + +For an API reference, you can check out the pages for [circuits](api/circuits_api.md) and for [nodes](api/nodes_api.md). diff --git a/docs/tutorial_running_exps.md b/docs/tutorial_running_exps.md new file mode 100644 index 0000000..593bd22 --- /dev/null +++ b/docs/tutorial_running_exps.md @@ -0,0 +1,3 @@ +# Tutorial: Running experiments +!!! failure + This section is currently missing. Please see the [code generation API](api/code_generation_api.md) for some documentation for now. diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..3549e0f --- /dev/null +++ b/main.cpp @@ -0,0 +1,59 @@ + +#include +#include +#include +#include + +#include + +typedef helib::Ptxt ptxt_t; +typedef helib::Ctxt ctxt_t; + +std::map input_map; + +void parse_arguments(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + std::string argument(argv[i]); + size_t pos = argument.find('='); + if (pos != std::string::npos) { + std::string key = argument.substr(0, pos); + int value = std::stoi(argument.substr(pos + 1)); + input_map[key] = value; + } + } +} + +int extract_input(const std::string& name) { + if (input_map.find(name) != input_map.end()) { + return input_map[name]; + } else { + std::cerr << "Error: " << name << " not found" << std::endl; + return -1; + } +} + +int main(int argc, char* argv[]) { + // Parse the inputs + parse_arguments(argc, argv); + + // Set up the HE parameters + unsigned long p = 257; + unsigned long m = 65536; + unsigned long r = 1; + unsigned long bits = 449; + unsigned long c = 3; + helib::Context context = helib::ContextBuilder() + .m(m) + .p(p) + .r(r) + .bits(bits) + .c(c) + .build(); + + + // Generate keys + helib::SecKey secret_key(context); + secret_key.GenSecKey(); + helib::addSome1DMatrices(secret_key); + const helib::PubKey& public_key = secret_key; + diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..417bff0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,57 @@ +site_name: Oraqle + +nav: + - index.md + - getting_started.md + - tutorial_running_exps.md + - API reference: + - api/circuits_api.md + - api/nodes_api.md + - api/code_generation_api.md + - api/pareto_fronts_api.md + - api/abstract_nodes_api.md + - api/addition_chains_api.md + - example_circuits.md + - config.md + +plugins: +- search +- mkdocstrings: + handlers: + python: + options: + show_root_heading: true + allow_inspection: false + show_submodules: false + show_root_full_path: false + show_symbol_type_heading: true + # show_symbol_type_toc: true This currently causes a bug + docstring_style: google + follow_wrapped_lines: true + crosslink_types: true # Makes types clickable + crosslink_types_style: 'sphinx' # Default or sphinx style + annotations_path: brief + inherited_members: true + members_order: source + show_if_no_docstring: true + separate_signature: false + show_source: false + docstring_section_style: list + +theme: + name: material + highlightjs: true + +markdown_extensions: + - admonition + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.critic + - pymdownx.details + - pymdownx.tasklist + - pymdownx.tabbed + - pymdownx.magiclink + - pymdownx.tilde + - toc: + permalink: true + toc_depth: 3 diff --git a/oraqle/__init__.py b/oraqle/__init__.py new file mode 100644 index 0000000..7b975e3 --- /dev/null +++ b/oraqle/__init__.py @@ -0,0 +1 @@ +"""This module contains the oraqle compiler, tools, and example circuits.""" diff --git a/oraqle/add_chains/__init__.py b/oraqle/add_chains/__init__.py new file mode 100644 index 0000000..e1a4ca8 --- /dev/null +++ b/oraqle/add_chains/__init__.py @@ -0,0 +1 @@ +"""Tools for generating addition chains using different constraints and objectives.""" diff --git a/oraqle/add_chains/addition_chains.py b/oraqle/add_chains/addition_chains.py new file mode 100644 index 0000000..a66bbd3 --- /dev/null +++ b/oraqle/add_chains/addition_chains.py @@ -0,0 +1,283 @@ +"""Tools for generating short addition chains using a MaxSAT formulation.""" +import math +from typing import List, Optional, Tuple + +from pysat.card import CardEnc +from pysat.formula import WCNF + +from oraqle.add_chains.memoization import ADDCHAIN_CACHE_PATH, cache_to_disk +from oraqle.add_chains.solving import solve, solve_with_time_limit +from oraqle.config import MAXSAT_TIMEOUT + + +def thurber_bounds(target: int, max_size: int) -> List[Tuple[int, int]]: + """Returns the Thurber bounds for a given target and a maximum size of the addition chain.""" + m = target + t = 0 + while (m % 2) == 0: + t += 1 + m >>= 1 + + bounds = [] + for step in range(max_size - t - 3 + 1): + if ((1 << (max_size - t - step - 2) + 1) % target) == 0: + denominator = (1 << (t + 1)) * ((1 << (max_size - t - (step + 2))) + 1) + else: + denominator = (1 << t) * ((1 << (max_size - t - (step + 1))) + 1) + bound = int(math.ceil(target / denominator)) + bounds.append((bound, min(1 << step, target))) + + step = max_size - t - 2 + if step > 0: + denominator = (1 << t) * ((1 << (max_size - t - (step + 1))) + 1) + bound = int(math.ceil(target / denominator)) + bounds.append((bound, min(1 << step, target))) + + if max_size - t - 1 > 0: + for step in range(max_size - t - 1, max_size + 1): + bound = int(math.ceil(target / (1 << (max_size - step)))) + bounds.append((bound, min(1 << step, target))) + + return bounds + + +@cache_to_disk(ADDCHAIN_CACHE_PATH, ignore_args={"solver", "encoding", "thurber"}) +def add_chain( # noqa: PLR0912, PLR0913, PLR0915, PLR0917 + target: int, + max_depth: Optional[int], + strict_cost_max: float, + squaring_cost: float, + solver: str, + encoding: int, + thurber: bool, + min_size: int, + precomputed_values: Optional[Tuple[Tuple[int, int], ...]], +) -> Optional[List[Tuple[int, int]]]: + """Generates a minimum-cost addition chain for a given target, abiding to the constraints. + + Parameters: + target: The target integer. + max_depth: The maximum depth of the addition chain + strict_cost_max: A strict upper bound on the cost of the addition chain. I.e., cost(chain) < strict_cost_max. + squaring_cost: The cost of doubling (squaring), compared to other additions (multiplications), which cost 1.0. + solver: Name of the SAT solver, e.g. "glucose421" for glucose 4.2.1. See: https://pysathq.github.io/docs/html/api/solvers.html. + encoding: The encoding to use for cardinality constraints. See: https://pysathq.github.io/docs/html/api/card.html#pysat.card.EncType. + thurber: Whether to use the Thurber bounds, which provide lower bounds for the elements in the chain. The bounds are ignored when `precomputed_values = True`. + min_size: The minimum size of the chain. It is always possible to use `math.ceil(math.log2(target))`. + precomputed_values: If there are any precomputed values that can be used for free, they can be specified as a tuple of pairs (value, chain_depth). + + Raises: # noqa: DOC502 + TimeoutError: If the global MAXSAT_TIMEOUT is not None, and it is reached before a maxsat instance could be solved. + + Returns: + A minimum-cost addition chain, if it exists. + """ + # TODO: Maybe precomputed_values should not be optional, but should be ignored if it is empty + assert target != 0 + + if target == 1: + return [] + + def x(i) -> int: + return i + + if precomputed_values is not None: + + def z(i: int) -> int: + offset = target + 1 + return i + offset + + def y(i, j) -> int: + # TODO: We can make the offset tighter + offset = (target + 1) if precomputed_values is None else 2 * (target + 1) + assert i <= j + return j * (j + 1) // 2 + i + offset + + def y_inv(n: int) -> Tuple[int, int]: + offset = (target + 1) if precomputed_values is None else 2 * (target + 1) + assert n >= offset + n -= offset + j = math.floor((math.sqrt(1 + 8 * n) - 1) // 2) + i = n - j * (j + 1) // 2 + + return i, j # minus 1 so that 1 -> 0 + + if max_depth is not None: + + def d(i, depth) -> int: + offset = y(target, target) + assert depth <= max_depth + 1 + return offset + 1 + (i - 1) * (max_depth + 1) + depth + + wcnf = WCNF() + + # x_i for i = 1,...,target represents the computed additions + # y_i,j for i,j = 2,...,target s.t. i <= j represents that i+j is computed + + # Add constraints + big_disjunctions = {k: [] for k in range(1, target + 1)} + for j in range(1, target + 1): + x_j = x(j) + + for i in range(1, min(j + 1, target + 1 - j)): + x_i = x(i) + y_ij = y(i, j) + + k = i + j + + # y_ij requires that x_i is set + wcnf.append([-y_ij, x_i]) + if i != j: + # y_ij requires that x_j is set + wcnf.append([-y_ij, x_j]) + + # x_k is set when y_ij is set + big_disjunctions[k].append(y_ij) + + # Add objective + wcnf.append([-y(i, j)], weight=(squaring_cost if i == j else 1)) + + if max_depth is not None: + for depth in range(max_depth + 1): + # d_k,depth+1 is set when d_i,depth and y_ij are set + wcnf.append([d(k, depth + 1), -d(i, depth), -y_ij]) + if i != j: + # d_k,depth+1 is set when d_j,depth and y_ij are set + wcnf.append([d(k, depth + 1), -d(j, depth), -y_ij]) + + if precomputed_values is not None: + for k, k_depth in precomputed_values: + if k == 0 or k > target: + continue + + if max_depth is not None and k_depth > max_depth: + continue + + # x_k is set when z_k is set + big_disjunctions[k].append(z(k)) + + if max_depth is not None: + wcnf.append([d(k, k_depth), -z(k)]) + + wcnf.append([x(target)]) + + if max_depth is not None: + wcnf.append([d(1, 0)]) + + for k in range(2, target + 1): + big_disjunctions[k].append(-x(k)) + wcnf.append(big_disjunctions[k]) + + # Cut some potential additions + if precomputed_values is None: + # We do not use these bounds when precomputed_values is not None + wcnf.append([x(m) for m in range((k + 1) // 2, k)]) # type: ignore + + if max_depth is not None: + # May not exceed max_depth + wcnf.append([-d(k, max_depth + 1)]) + + # Add generalized Thurber bounds (for each step in the chain, the number must be between lower_bound and 2^step) + # We do not use the Thurber bounds when precomputed_values is not None + if thurber and precomputed_values is None: + max_size = math.floor(strict_cost_max / squaring_cost) + for lb, ub in thurber_bounds(target, max_size): + # FIXME: These bounds seem not to help for target ~ hundreds + wcnf.append([x(i) for i in range(lb, ub + 1)]) + + # Bound the number of x that are true from below + if max_depth is None: + top_id = y(target, target) + else: + top_id = y(target, target) + 1 + (target - 1) * (max_depth + 1) + max_depth + 1 + at_least_cnf = CardEnc.atleast( + [x(k) for k in range(2, target + 1)], bound=min_size, top_id=top_id, encoding=encoding + ) + wcnf.extend(at_least_cnf) + + # Solve + if MAXSAT_TIMEOUT is None: + model = solve(wcnf, solver, strict_cost_max) + else: + model = solve_with_time_limit(wcnf, solver, strict_cost_max, MAXSAT_TIMEOUT) + + if model is None: + return None + + offset = (target + 1) if precomputed_values is None else 2 * (target + 1) + return [y_inv(n) for n in model if offset <= n <= y(target, target)] + + +def test_addition_chain(): # noqa: D103 + chain = add_chain( + 8, + 3, + 2.0, + 0.5, + solver="glucose42", + encoding=1, + thurber=True, + min_size=2, + precomputed_values=None, + ) + assert chain == [(1, 1), (2, 2), (4, 4)] + + +def test_addition_chain_precomputed_no_depth(): # noqa: D103 + chain = add_chain( + 8, + None, + 2.0, + 0.5, + solver="glucose42", + encoding=1, + thurber=True, + min_size=1, + precomputed_values=((7, 2),), + ) + assert chain == [(1, 7)] + + +def test_addition_chain_precomputed_depth(): # noqa: D103 + chain = add_chain( + 8, + 3, + 2.0, + 0.5, + solver="glucose42", + encoding=1, + thurber=True, + min_size=1, + precomputed_values=((7, 2),), + ) + assert chain == [(1, 7)] + + +def test_addition_chain_precomputed_depth_too_large(): # noqa: D103 + chain = add_chain( + 8, + 3, + 2.0, + 0.5, + solver="glucose42", + encoding=1, + thurber=True, + min_size=1, + precomputed_values=((7, 3),), + ) + assert chain == [(1, 1), (2, 2), (4, 4)] + + +def test_addition_chain_precomputed_no_depth_squaring(): # noqa: D103 + chain = add_chain( + 18, + None, + 2.0, + 0.5, + solver="glucose42", + encoding=1, + thurber=True, + min_size=1, + precomputed_values=((9, 3),), + ) + assert chain == [(9, 9)] diff --git a/oraqle/add_chains/addition_chains_front.py b/oraqle/add_chains/addition_chains_front.py new file mode 100644 index 0000000..2776582 --- /dev/null +++ b/oraqle/add_chains/addition_chains_front.py @@ -0,0 +1,153 @@ +"""Tools for generating addition chains that trade off depth and cost.""" +import math +from typing import List, Optional, Tuple + +from oraqle.add_chains.addition_chains import add_chain +from oraqle.add_chains.addition_chains_mod import add_chain_modp, hw, size_lower_bound + + +def chain_depth( + chain: List[Tuple[int, int]], + precomputed_values: Optional[Tuple[Tuple[int, int], ...]] = None, + modulus: Optional[int] = None, +) -> int: + """Return the depth of the addition chain.""" + depths = {1: 0} + if precomputed_values is not None: + depths.update(precomputed_values) + + if modulus is None: + for x, y in chain: + depths[x + y] = max(depths[x], depths[y]) + 1 + else: + for x, y in chain: + depths[(x + y) % modulus] = max(depths[x % modulus], depths[y % modulus]) + 1 + + return max(depths.values()) + + +def gen_pareto_front( # noqa: PLR0912, PLR0913, PLR0917 + target: int, + modulus: Optional[int], + squaring_cost: float, + solver="glucose42", + encoding=1, + thurber=True, + precomputed_values: Optional[Tuple[Tuple[int, int], ...]] = None, +) -> List[Tuple[int, List[Tuple[int, int]]]]: + """Returns a Pareto front of addition chains, trading of cost and depth.""" + if target == 1: + return [(0, [])] + + if modulus is not None: + assert target <= modulus + + # Find the lowest depth chain using square & multiply (SaM) + sam_depth = math.ceil(math.log2(target)) + sam_cost = math.ceil(math.log2(target)) * squaring_cost + hw(target) - 1 + sam_target = target + + # If there is a modulus, we should also consider it to find an upper bound on the cost of a minimum-depth chain + if modulus is not None: + current_target = target + modulus - 1 + while math.log2(current_target) <= sam_depth: + current_cost = ( + math.ceil(math.log2(current_target)) * squaring_cost + hw(current_target) - 1 + ) + if current_cost < sam_cost: + sam_cost = current_cost + sam_target = target + current_target += modulus - 1 + + # Find the cheapest chain (i.e. no depth constraints) + min_size = size_lower_bound(target) if precomputed_values is None else 1 + if modulus is None: + cheapest_chain = add_chain( + target, + None, + sam_cost, + squaring_cost, + solver, + encoding, + thurber, + min_size, + precomputed_values, + ) + else: + cheapest_chain = add_chain_modp( + target, + modulus, + None, + sam_cost, + squaring_cost, + solver, + encoding, + thurber, + min_size, + precomputed_values, + ) + + # If no cheapest chain is found that satisfies these bounds, then square and multiply had the same cost + if cheapest_chain is None: + sam_chain = [] + for i in range(math.ceil(math.log2(sam_target))): + sam_chain.append((2**i, 2**i)) + previous = 1 + for i in range(math.ceil(math.log2(sam_target))): + if (sam_target >> i) & 1: + sam_chain.append((previous, 2**i)) + previous += 2**i + return [(sam_depth, sam_chain)] + + add_size = len(cheapest_chain) # TODO: Check that this is indeed a valid bound + add_cost = sum(squaring_cost if x == y else 1.0 for x, y in cheapest_chain) + add_depth = chain_depth(cheapest_chain, precomputed_values, modulus=modulus) + + # Go through increasing depth and decrease the previous size, until we reach the cost of square and multiply + pareto_front = [] + current_depth = sam_depth + current_cost = sam_cost + while current_cost > add_cost and current_depth < add_depth: + if modulus is None: + chain = add_chain( + target, + current_depth, + current_cost, + squaring_cost, + solver, + encoding, + thurber, + add_size, + precomputed_values, + ) + else: + chain = add_chain_modp( + target, + modulus, + current_depth, + current_cost, + squaring_cost, + solver, + encoding, + thurber, + add_size, + precomputed_values, + ) + + if chain is not None: + # Add to the Pareto front + pareto_front.append((current_depth, chain)) + current_cost = sum(squaring_cost if x == y else 1.0 for x, y in chain) + + current_depth += 1 + + # Add the final chain and return + if add_cost < current_cost or len(pareto_front) == 0: + pareto_front.append((add_depth, cheapest_chain)) + + return pareto_front + + +def test_gen_exponentiation_front_small(): # noqa: D103 + front = gen_pareto_front(2, None, 0.75) + assert front == [(1, [(1, 1)])] diff --git a/oraqle/add_chains/addition_chains_heuristic.py b/oraqle/add_chains/addition_chains_heuristic.py new file mode 100644 index 0000000..70ec347 --- /dev/null +++ b/oraqle/add_chains/addition_chains_heuristic.py @@ -0,0 +1,143 @@ +"""This module contains functions for finding addition chains, while sometimes resorting to heuristics to prevent long computations.""" + +from functools import lru_cache +import math +from typing import List, Optional, Tuple + +from oraqle.add_chains.addition_chains import add_chain +from oraqle.add_chains.addition_chains_mod import add_chain_modp, hw +from oraqle.add_chains.solving import extract_indices + + +def _mul(current_chain: List[Tuple[int, int]], other_chain: List[Tuple[int, int]]): + length = len(current_chain) + for a, b in other_chain: + current_chain.append((a + length, b + length)) + + +def _chain(n, k) -> List[Tuple[int, int]]: + q = n // k + r = n % k + if r in {0, 1}: + chain_k = _minchain(k) + _mul(chain_k, _minchain(q)) + if r == 1: + chain_k.append((0, len(chain_k))) + return chain_k + else: + chain_k = _chain(k, r) + index_r = len(chain_k) + _mul(chain_k, _minchain(q)) + chain_k.append((index_r, len(chain_k))) + return chain_k + + +def _minchain(n: int) -> List[Tuple[int, int]]: + log_n = n.bit_length() - 1 + if n == 1 << log_n: + return [(i, i) for i in range(log_n)] + elif n == 3: + return [(0, 0), (0, 1)] + else: + k = n // (1 << (log_n // 2)) + return _chain(n, k) + + +@lru_cache +def add_chain_guaranteed( # noqa: PLR0913, PLR0917 + target: int, + modulus: Optional[int], + squaring_cost: float, + solver: str = "glucose421", + encoding: int = 1, + thurber: bool = True, + precomputed_values: Optional[Tuple[Tuple[int, int], ...]] = None, +) -> List[Tuple[int, int]]: + """Always generates an addition chain for a given target, which is suboptimal if the inputs are too large. + + In some cases, the result is not necessarily optimal. These are the cases where we resort to a heuristic. + This currently happens if: + - The target exceeds 1000. + - The modulus (if provided) exceeds 200. + - MAXSAT_TIMEOUT is not None and a MaxSAT instance timed out + + !!! note + This function is useful for preventing long computation, but the result is not guaranteed to be (close to) optimal. + Unlike `add_chain`, this function will always return an addition chain. + + Parameters: + target: The target integer. + modulus: Modulus to take into account. In an exponentiation chain, this is the modulus in the exponent, i.e. x^target mod p corresponds to `modulus = p - 1`. + squaring_cost: The cost of doubling (squaring), compared to other additions (multiplications), which cost 1.0. + solver: Name of the SAT solver, e.g. "glucose421" for glucose 4.2.1. See: https://pysathq.github.io/docs/html/api/solvers.html. + encoding: The encoding to use for cardinality constraints. See: https://pysathq.github.io/docs/html/api/card.html#pysat.card.EncType. + thurber: Whether to use the Thurber bounds, which provide lower bounds for the elements in the chain. The bounds are ignored when `precomputed_values = True`. + precomputed_values: If there are any precomputed values that can be used for free, they can be specified as a tuple of pairs (value, chain_depth). + + Raises: # noqa: DOC502 + TimeoutError: If the global MAXSAT_TIMEOUT is not None, and it is reached before a maxsat instance could be solved. + + Returns: + An addition chain. + """ + # We want to do better than square and multiply, so we find an upper bound + sam_cost = math.ceil(math.log2(target)) * squaring_cost + hw(target) - 1 + + # Apply CSE to the square & mutliply chain + if precomputed_values is not None: + for exp, depth in precomputed_values: + if (exp & (exp - 1)) == 0 and depth == math.log2(exp): + sam_cost -= squaring_cost + + try: + addition_chain = None + if modulus is not None and modulus <= 200: + addition_chain = add_chain_modp( + target, + modulus, + None, + sam_cost, + squaring_cost, + solver, + encoding, + thurber, + min_size=math.ceil(math.log2(target)) if precomputed_values is None else 1, + precomputed_values=precomputed_values, + ) + elif target <= 1000: + addition_chain = add_chain( + target, + None, + sam_cost, + squaring_cost, + solver, + encoding, + thurber, + min_size=math.ceil(math.log2(target)) if precomputed_values is None else 1, + precomputed_values=precomputed_values, + ) + + if addition_chain is not None: + addition_chain = extract_indices( + addition_chain, precomputed_values=None if precomputed_values is None else list(k for k, _ in precomputed_values), modulus=modulus + ) + except TimeoutError: + # The MaxSAT solver timed out, so we resort to a heuristic + pass + + if addition_chain is None: + # If no other addition chain algorithm has been called or if we could not do better than square and multiply + + # Uses the minchain algorithm from ["Addition chains using continued fractions."][BBBD1989] + # The implementation was adapted from the `addchain` Rust crate (https://github.com/str4d/addchain). + # This algorithm is not optimal: Below 1000 it requires one too many multiplication in 29 cases. + addition_chain = _minchain(target) + + if precomputed_values is not None: + # We must shift the indices in the addition chain + shift = len(precomputed_values) + addition_chain = [(0 if x == 0 else x + shift, 0 if y == 0 else y + shift) for (x, y) in addition_chain] + + assert addition_chain is not None + + return addition_chain diff --git a/oraqle/add_chains/addition_chains_mod.py b/oraqle/add_chains/addition_chains_mod.py new file mode 100644 index 0000000..08c4181 --- /dev/null +++ b/oraqle/add_chains/addition_chains_mod.py @@ -0,0 +1,164 @@ +"""Tools for computing addition chains, taking into account the modular nature of the algebra.""" +import math +from typing import List, Optional, Tuple + +from oraqle.add_chains.addition_chains import add_chain + + +def hw(n: int) -> int: + """Returns the Hamming weight of n.""" + c = 0 + while n: + c += 1 + n &= n - 1 + + return c + + +def size_lower_bound(target: int) -> int: + """Returns a lower bound on the size of the addition chain for this target.""" + return math.ceil( + max( + math.log2(target) + math.log2(hw(target)) - 2.13, + math.log2(target), + math.log2(target) + math.log(hw(target), 3) - 1, + ) + ) + + +def cost_lower_bound_monotonic(target: int, squaring_cost: float) -> float: + """Returns a lower bound on the cost of the addition chain for this target. The bound is guaranteed to grow monotonically with the target.""" + return math.ceil(math.log2(target)) * squaring_cost + + +def chain_cost(chain: List[Tuple[int, int]], squaring_cost: float) -> float: + """Returns the cost of the addition chain, considering doubling (squaring) to be cheaper than other additions (multiplications).""" + return sum(squaring_cost if x == y else 1.0 for x, y in chain) + + +def add_chain_modp( # noqa: PLR0913, PLR0917 + target: int, + modulus: int, + max_depth: Optional[int], + strict_cost_max: float, + squaring_cost: float, + solver, + encoding, + thurber, + min_size: int, + precomputed_values: Optional[Tuple[Tuple[int, int], ...]] = None, +) -> Optional[List[Tuple[int, int]]]: + """Computes an addition chain for target modulo p with the given constraints and optimization parameters. + + The precomputed_powers are an optional set of powers that have previously been computed along with their depth. + This means that those powers can be reused for free. + + Returns: + If it exists, a minimal addition chain meeting the given constraints and optimization parameters. + """ + if precomputed_values is not None: + # The shortest chain in (t + (k-1)p, t + kp] will have length at least k + # The cheapest chain in (t + (k-1)p, t + kp] will have cost at least k / sqr_cost + best_chain = None + + k = 0 + while (k / squaring_cost) < strict_cost_max: + # Add multiples of the precomputed_values + new_precomputed_values = [] + for precomputed_value, depth in precomputed_values: + for i in range(k + 1): + new_precomputed_values.append((precomputed_value + i * modulus, depth)) + + chain = add_chain( + target + k * modulus, + max_depth, + strict_cost_max, + squaring_cost, + solver, + encoding, + thurber, + min_size=max(min_size, k), + precomputed_values=tuple(new_precomputed_values), + ) + + if chain is not None: + cost = chain_cost(chain, squaring_cost) + strict_cost_max = min(strict_cost_max, cost) + best_chain = chain + + k += 1 + + return best_chain + + best_chain = None + best_cost = None + + current_target = target + + i = 0 + + while cost_lower_bound_monotonic(current_target, squaring_cost) < strict_cost_max and ( + max_depth is None or math.ceil(math.log2(current_target)) <= max_depth + ): + tightest_min_size = max(size_lower_bound(current_target), min_size) + if (tightest_min_size * squaring_cost) >= ( + strict_cost_max if best_cost is None else min(strict_cost_max, best_cost) + ): + current_target += modulus + continue + + chain = add_chain( + current_target, + max_depth, + strict_cost_max, + squaring_cost, + solver, + encoding, + thurber, + tightest_min_size, + precomputed_values, + ) + + if chain is not None: + cost = chain_cost(chain, squaring_cost) + if best_cost is None or cost < best_cost: + best_cost = cost + best_chain = chain + strict_cost_max = min(best_cost, strict_cost_max) + + current_target += modulus + + i += 1 + return best_chain + + +def test_add_chain_modp_over_modulus(): # noqa: D103 + chain = add_chain_modp( + 62, + 66, + None, + 8.0, + 0.75, + solver="glucose42", + encoding=1, + thurber=True, + min_size=1, + precomputed_values=None, + ) + assert chain == [(1, 1), (2, 2), (4, 4), (8, 8), (16, 16), (32, 32), (64, 64)] + + +def test_add_chain_modp_precomputations(): # noqa: D103 + chain = add_chain_modp( + 64, # 64+66 = 65+65 + 66, + None, + 2.0, + 0.75, + solver="glucose42", + encoding=1, + thurber=True, + min_size=1, + precomputed_values=((65, 5),), + ) + assert chain == [(65, 65)] diff --git a/oraqle/add_chains/memoization.py b/oraqle/add_chains/memoization.py new file mode 100644 index 0000000..e236888 --- /dev/null +++ b/oraqle/add_chains/memoization.py @@ -0,0 +1,58 @@ +"""This module contains tools for memoizing addition chains, as these are expensive to compute.""" +from hashlib import sha3_256 +import inspect +import shelve +from typing import Set + +from sympy import sieve + + +ADDCHAIN_CACHE_PATH = "addchain_cache" + + +# Adapted from: https://stackoverflow.com/questions/16463582/memoize-to-disk-python-persistent-memoization +def cache_to_disk(file_name, ignore_args: Set[str]): + """This decorator caches the calls to this function in a file on disk, ignoring the arguments listed in `ignore_args`.""" + d = shelve.open(file_name) # noqa: SIM115 + + def decorator(func): + signature = inspect.signature(func) + signature_args = list(signature.parameters.keys()) + assert all(arg in signature_args for arg in ignore_args) + + def wrapped_func(*args, **kwargs): + relevant_args = [a for a, sa in zip(args, signature_args) if sa not in ignore_args] + for kwarg in signature_args[len(args):]: + if kwarg not in ignore_args: + relevant_args.append(kwargs[kwarg]) + + h = sha3_256() + h.update(str(relevant_args).encode('ascii')) + hashed_args = h.hexdigest() + + if hashed_args not in d: + d[hashed_args] = func(*args, **kwargs) + return d[hashed_args] + + return wrapped_func + + return decorator # noqa: DOC201 + + +if __name__ == "__main__": + from oraqle.add_chains.addition_chains_front import gen_pareto_front + + # Precompute addition chains for x^(p-1) mod p for the first 30 primes p + primes = list(sieve.primerange(300))[:30] + for sqr_cost in [0.5, 0.75, 1.0]: + print(f"Computing for {sqr_cost}") + + for p in primes: + gen_pareto_front( + p - 1, + modulus=p - 1, + squaring_cost=sqr_cost, + solver="glucose42", + encoding=1, + thurber=True, + ) diff --git a/oraqle/add_chains/solving.py b/oraqle/add_chains/solving.py new file mode 100644 index 0000000..2cb29cd --- /dev/null +++ b/oraqle/add_chains/solving.py @@ -0,0 +1,139 @@ +"""Tools for solving SAT formulations.""" +import math +import signal +from typing import List, Optional, Sequence, Tuple + +from pysat.examples.rc2 import RC2 +from pysat.formula import WCNF + + +def solve(wcnf: WCNF, solver: str, strict_cost_max: Optional[float]) -> Optional[List[int]]: + """This code is adapted from pysat's internal code to stop when we have reached a maximum cost. + + Returns: + A list containing the assignment (where 3 indicates that 3=True and -3 indicates that 3=False), or None if the wcnf is unsatisfiable. + """ + rc2 = RC2(wcnf, solver) + + if strict_cost_max is None: + strict_cost_max = float("inf") + + while not rc2.oracle.solve(assumptions=rc2.sels + rc2.sums): # type: ignore + rc2.get_core() + + if not rc2.core: + # core is empty, i.e. hard part is unsatisfiable + return None + + rc2.process_core() + + if rc2.cost >= strict_cost_max: + return None + + rc2.model = rc2.oracle.get_model() # type: ignore + + # Return None if the model could not be solved + if rc2.model is None: + return None + + # Extract the model + if rc2.model is None and rc2.pool.top == 0: + # we seem to have been given an empty formula + # so let's transform the None model returned to [] + rc2.model = [] + + rc2.model = filter(lambda inp: abs(inp) in rc2.vmap.i2e, rc2.model) # type: ignore + rc2.model = map(lambda inp: int(math.copysign(rc2.vmap.i2e[abs(inp)], inp)), rc2.model) + rc2.model = sorted(rc2.model, key=abs) + + return rc2.model + + +def extract_indices( + sequence: List[Tuple[int, int]], + precomputed_values: Optional[Sequence[int]] = None, + modulus: Optional[int] = None, +) -> List[Tuple[int, int]]: + """Returns the indices for each step of the addition chain. + + If n precomputed values are provided, then these are considered to be the first n indices after x (i.e. x has index 0, followed by 1, ..., n representing the precomputed values). + """ + indices = {1: 0} + offset = 1 + if precomputed_values is not None: + for v in precomputed_values: + indices[v] = offset + offset += 1 + ans_sequence = [] + + if modulus is None: + for index, pair in enumerate(sequence): + i, j = pair + ans_sequence.append((indices[i], indices[j])) + indices[i + j] = index + offset + else: + for index, pair in enumerate(sequence): + i, j = pair + ans_sequence.append((indices[i % modulus], indices[j % modulus])) + indices[(i + j) % modulus] = index + offset + + return ans_sequence + + +def solve_with_time_limit(wcnf: WCNF, solver: str, strict_cost_max: Optional[float], timeout_secs: float) -> Optional[List[int]]: + """This code is adapted from pysat's internal code to stop when we have reached a maximum cost. + + Raises: # noqa: DOC502 + TimeoutError: When a timeout occurs (after `timeout_secs` seconds) + + Returns: + A list containing the assignment (where 3 indicates that 3=True and -3 indicates that 3=False), or None if the wcnf is unsatisfiable. + """ + def timeout_handler(s, f): + raise TimeoutError + + # Set the timeout + signal.signal(signal.SIGALRM, timeout_handler) + signal.setitimer(signal.ITIMER_REAL, timeout_secs) + + try: + # TODO: Reduce code duplication: we only changed solve to solve_limited + rc2 = RC2(wcnf, solver) + + if strict_cost_max is None: + strict_cost_max = float("inf") + + while not rc2.oracle.solve_limited(assumptions=rc2.sels + rc2.sums, expect_interrupt=True): # type: ignore + rc2.get_core() + + if not rc2.core: + # core is empty, i.e. hard part is unsatisfiable + signal.setitimer(signal.ITIMER_REAL, 0) + return None + + rc2.process_core() + + if rc2.cost >= strict_cost_max: + signal.setitimer(signal.ITIMER_REAL, 0) + return None + + signal.setitimer(signal.ITIMER_REAL, 0) + rc2.model = rc2.oracle.get_model() # type: ignore + + # Return None if the model could not be solved + if rc2.model is None: + return None + + # Extract the model + if rc2.model is None and rc2.pool.top == 0: + # we seem to have been given an empty formula + # so let's transform the None model returned to [] + rc2.model = [] + + rc2.model = filter(lambda inp: abs(inp) in rc2.vmap.i2e, rc2.model) # type: ignore + rc2.model = map(lambda inp: int(math.copysign(rc2.vmap.i2e[abs(inp)], inp)), rc2.model) + rc2.model = sorted(rc2.model, key=abs) + + return rc2.model + except TimeoutError as err: + raise TimeoutError from err # noqa: DOC501 diff --git a/oraqle/circuits/__init__.py b/oraqle/circuits/__init__.py new file mode 100644 index 0000000..d0582c2 --- /dev/null +++ b/oraqle/circuits/__init__.py @@ -0,0 +1 @@ +"""This package contains example circuits and tools for generating them.""" diff --git a/oraqle/circuits/aes.py b/oraqle/circuits/aes.py new file mode 100644 index 0000000..e2832fa --- /dev/null +++ b/oraqle/circuits/aes.py @@ -0,0 +1,87 @@ +"""This module implements a high-level AES encryption circuit for a constant key.""" +from typing import List + +from aeskeyschedule import key_schedule +from galois import GF + +from oraqle.compiler.arithmetic.exponentiation import Power +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes import Constant +from oraqle.compiler.nodes.abstract import Node +from oraqle.compiler.nodes.leafs import Input + +gf = GF(2**8) + + +def encrypt(plaintext: List[Node], key: bytes) -> List[Node]: + """Returns an AES encryption circuit for a constant `key`.""" + mix = [Constant(gf(2)), Constant(gf(3)), Constant(gf(1)), Constant(gf(1))] + + round_keys = [[Constant(gf(byte)) for byte in round_key] for round_key in key_schedule(key)] + + def additions(nodes: List[Node]) -> Node: + node_iter = iter(nodes) + out = next(node_iter) + next(node_iter) + for node in node_iter: + out += node + return out + + def sbox(node: Node, method="minchain") -> Node: + if method == "hardcoded": + x2 = node.mul(node, flatten=False) + x3 = node.mul(x2, flatten=False) + x6 = x3.mul(x3, flatten=False) + x12 = x6.mul(x6, flatten=False) + x15 = x12.mul(x3, flatten=False) + x30 = x15.mul(x15, flatten=False) + x60 = x30.mul(x30, flatten=False) + x63 = x60.mul(x3, flatten=False) + x126 = x63.mul(x63, flatten=False) + x127 = node.mul(x126, flatten=False) + x254 = x127.mul(x127, flatten=False) + return x254 + elif method == "minchain": + return Power(node, 254, gf) + else: + raise Exception(f"Invalid method: {method}.") + + # AddRoundKey + b = [round_key + plaintext_byte for round_key, plaintext_byte in zip(round_keys[0], plaintext)] + + for round in range(9): + # SubBytes (modular inverse) + b = [sbox(b[j], method="hardcoded") for j in range(16)] + + # ShiftRows + b[1], b[5], b[9], b[13] = b[5], b[9], b[13], b[1] + b[2], b[6], b[10], b[14] = b[10], b[14], b[2], b[6] + b[3], b[7], b[11], b[15] = b[15], b[3], b[7], b[11] + + # MixColumns + b = [additions([mix[(j + i) % 4] * b[j // 4 + i] for i in range(4)]) for j in range(16)] + + # AddRoundKey + b = [round_key + b[j] for j, round_key in zip(range(16), round_keys[round + 1])] + b: List[Node] + + return b + + +if __name__ == "__main__": + # TODO: Consider if we want to support degree > 1 + circuit = Circuit( + encrypt([Input(f"{i}", gf) for i in range(16)], b"abcdabcdabcdabcd") + ).arithmetize() + print(circuit) + print(circuit.multiplicative_depth()) + print(circuit.multiplicative_size()) + circuit.eliminate_subexpressions() + print(circuit.multiplicative_depth()) + print(circuit.multiplicative_size()) + + # TODO: Test if it corresponds to a plaintext implementation of AES + + +def test_aes_128(): # noqa: D103 + # Only checks if no errors occur + Circuit(encrypt([Input(f"{i}", gf) for i in range(16)], b"abcdabcdabcdabcd")).arithmetize() diff --git a/oraqle/circuits/cardio.py b/oraqle/circuits/cardio.py new file mode 100644 index 0000000..969a260 --- /dev/null +++ b/oraqle/circuits/cardio.py @@ -0,0 +1,128 @@ +"""This module implements the cardio circuit that is often used in benchmarking compilers, see: https://arxiv.org/abs/2101.07078.""" +from typing import Type +from galois import GF, FieldArray + +from oraqle.compiler.boolean.bool_neg import Neg +from oraqle.compiler.boolean.bool_or import any_ +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes import Input +from oraqle.compiler.nodes.abstract import Node +from oraqle.compiler.nodes.arbitrary_arithmetic import sum_ + + +def construct_cardio_risk_circuit(gf: Type[FieldArray]) -> Node: + """Returns the cardio circuit from https://arxiv.org/abs/2101.07078.""" + man = Input("man", gf) + woman = Input("woman", gf) + smoking = Input("smoking", gf) + age = Input("age", gf) + diabetic = Input("diabetic", gf) + hbp = Input("hbp", gf) + cholesterol = Input("cholesterol", gf) + weight = Input("weight", gf) + height = Input("height", gf) + activity = Input("activity", gf) + alcohol = Input("alcohol", gf) + + return sum_( + man & (age > 50), + woman & (age > 60), + smoking, + diabetic, + hbp, + cholesterol < 40, + weight > (height - 90), # This might underflow if the modulus is too small + activity < 30, + man & (alcohol > 3), + Neg(man, gf) & (alcohol > 2), + ) + + +def construct_cardio_elevated_risk_circuit(gf: Type[FieldArray]) -> Node: + """Returns a variant of the cardio circuit that returns a Boolean indicating whether any risk factor returned true.""" + man = Input("man", gf) + woman = Input("woman", gf) + smoking = Input("smoking", gf) + age = Input("age", gf) + diabetic = Input("diabetic", gf) + hbp = Input("hbp", gf) + cholesterol = Input("cholesterol", gf) + weight = Input("weight", gf) + height = Input("height", gf) + activity = Input("activity", gf) + alcohol = Input("alcohol", gf) + + return any_( + man & (age > 50), + woman & (age > 60), + smoking, + diabetic, + hbp, + cholesterol < 40, + weight > (height - 90), # This might underflow if the modulus is too small + activity < 30, + man & (alcohol > 3), + Neg(man, gf) & (alcohol > 2), + ) + + +def test_cardio_p101(): # noqa: D103 + gf = GF(101) + circuit = Circuit([construct_cardio_risk_circuit(gf)]) + + for _, _, arithmetization in circuit.arithmetize_depth_aware(): + assert arithmetization.evaluate({ + "man": gf(1), + "woman": gf(0), + "age": gf(50), + "smoking": gf(0), + "diabetic": gf(0), + "hbp": gf(0), + "cholesterol": gf(45), + "weight": gf(10), + "height": gf(100), + "activity": gf(90), + "alcohol": gf(3), + })[0] == 0 + + assert arithmetization.evaluate({ + "man": gf(0), + "woman": gf(1), + "age": gf(50), + "smoking": gf(0), + "diabetic": gf(0), + "hbp": gf(0), + "cholesterol": gf(45), + "weight": gf(10), + "height": gf(100), + "activity": gf(90), + "alcohol": gf(3), + })[0] == 1 + + assert arithmetization.evaluate({ + "man": gf(1), + "woman": gf(0), + "age": gf(50), + "smoking": gf(0), + "diabetic": gf(0), + "hbp": gf(0), + "cholesterol": gf(39), + "weight": gf(10), + "height": gf(100), + "activity": gf(90), + "alcohol": gf(3), + })[0] == 1 + + assert arithmetization.evaluate({ + "man": gf(1), + "woman": gf(0), + "age": gf(50), + "smoking": gf(1), + "diabetic": gf(0), + "hbp": gf(0), + "cholesterol": gf(45), + "weight": gf(10), + "height": gf(100), + "activity": gf(90), + "alcohol": gf(3), + })[0] == 1 diff --git a/oraqle/circuits/median.py b/oraqle/circuits/median.py new file mode 100644 index 0000000..ccc5847 --- /dev/null +++ b/oraqle/circuits/median.py @@ -0,0 +1,30 @@ +"""This module implements circuits for computing the median.""" +from typing import Sequence, Type + +from galois import GF, FieldArray + +from oraqle.circuits.sorting import cswp +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes import Input + +gf = GF(1037347783) + + +def gen_median_circuit(inputs: Sequence[int], gf: Type[FieldArray]): + """Returns a naive circuit for finding the median value of `inputs`.""" + input_nodes = [Input(f"Input {v}", gf) for v in inputs] + + outputs = [n for n in input_nodes] + + for i in range(len(outputs) - 1, -1, -1): + for j in range(i): + outputs[j], outputs[j + 1] = cswp(outputs[j], outputs[j + 1]) # type: ignore + + if len(outputs) % 2 == 1: + return Circuit([outputs[len(outputs) // 2]]) + return Circuit([outputs[len(outputs) // 2 + 1]]) + + +if __name__ == "__main__": + circuit = gen_median_circuit(range(10), gf) + circuit.to_graph("median.dot") diff --git a/oraqle/circuits/mimc.py b/oraqle/circuits/mimc.py new file mode 100644 index 0000000..4521c88 --- /dev/null +++ b/oraqle/circuits/mimc.py @@ -0,0 +1,51 @@ +"""MIMC is an MPC-friendly cipher: https://eprint.iacr.org/2016/492.""" +from math import ceil, log2 +from random import randint + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes import Constant, Input, Node + +gf = GF(680564733841876926926749214863536422929) + + +# TODO: Check parameters with the paper +def encrypt(plaintext: Node, key: int, power_n: int = 129) -> Node: + """Returns an MIMC encryption circuit using a constant key.""" + rounds = ceil(power_n / log2(3)) + + constants = [ + ( + Constant(gf(0)) + if (round == 0) or (round == (rounds - 1)) + else Constant(gf(randint(0, 2**power_n))) + ) + for round in range(rounds) + ] + key_constant = Constant(gf(key)) + + for round in range(rounds): + added = plaintext + key_constant + constants[round] + plaintext = added * added * added + + return plaintext + key_constant + + +if __name__ == "__main__": + node = encrypt(Input("m", gf), 12345) + + circuit = Circuit([node]).arithmetize() + print(circuit.multiplicative_depth()) + print(circuit.multiplicative_size()) + + circuit.to_graph("mimc-129.dot") + + +def test_mimc_129(): # noqa: D103 + node = encrypt(Input("m", gf), 12345) + + circuit = Circuit([node]).arithmetize() + + assert circuit.multiplicative_depth() == 164 + assert circuit.multiplicative_size() == 164 diff --git a/oraqle/circuits/sorting.py b/oraqle/circuits/sorting.py new file mode 100644 index 0000000..b8e4233 --- /dev/null +++ b/oraqle/circuits/sorting.py @@ -0,0 +1,45 @@ +"""This module contains sorting circuits and comparators.""" +from typing import Sequence, Tuple, Type + +from galois import GF, FieldArray + +from oraqle.compiler.circuit import ArithmeticCircuit, Circuit +from oraqle.compiler.nodes import Input +from oraqle.compiler.nodes.abstract import Node + +gf = GF(13) + + +def cswp(lhs: Node, rhs: Node) -> Tuple[Node, Node]: + """Conditionally swap inputs `lhs` and `rhs` such that `lhs <= rhs`. + + Returns: + A tuple representing (lower, higher) + """ + teq = lhs < rhs + + first = teq * (lhs - rhs) + rhs + second = lhs + rhs - first + + return ( + first, + second, + ) + + +def gen_naive_sort_circuit(inputs: Sequence[int], gf: Type[FieldArray]) -> ArithmeticCircuit: + """Returns a naive sorting circuit for the given sequence of `inputs`.""" + input_nodes = [Input(f"Input {v}", gf) for v in inputs] + + outputs = [n for n in input_nodes] + + for i in range(len(outputs) - 1, -1, -1): + for j in range(i): + outputs[j], outputs[j + 1] = cswp(outputs[j], outputs[j + 1]) # type: ignore + + return Circuit(outputs).arithmetize() # type: ignore + + +if __name__ == "__main__": + circuit = gen_naive_sort_circuit(range(2), gf) + circuit.to_graph("sorting.dot") diff --git a/oraqle/circuits/veto_voting.py b/oraqle/circuits/veto_voting.py new file mode 100644 index 0000000..4c6af73 --- /dev/null +++ b/oraqle/circuits/veto_voting.py @@ -0,0 +1,27 @@ +"""The veto voting circuit is the inverse of a consensus vote between a number of participants. + +The circuit is essentially a large OR operation, returning 1 if any participant vetoes (by submitting a 1). +This represents a vote that anyone can veto. +""" +from typing import Type + +from galois import GF, FieldArray + +from oraqle.compiler.boolean.bool_or import any_ +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes import Input + +gf = GF(103) + + +def gen_veto_voting_circuit(participants: int, gf: Type[FieldArray]): + """Returns a veto voting circuit between the number of `participants`.""" + input_nodes = {Input(f"Input {i}", gf) for i in range(participants)} + return Circuit([any_(*input_nodes)]) + + +if __name__ == "__main__": + circuit = gen_veto_voting_circuit(10, gf).arithmetize() + + circuit.eliminate_subexpressions() + circuit.to_graph("veto-voting.dot") diff --git a/oraqle/compiler/__init__.py b/oraqle/compiler/__init__.py new file mode 100644 index 0000000..cd9ee1a --- /dev/null +++ b/oraqle/compiler/__init__.py @@ -0,0 +1 @@ +"""The compiler package contains the main machinery for describing high-level circuits, arithmetizing them, and generating code.""" diff --git a/oraqle/compiler/arithmetic/__init__.py b/oraqle/compiler/arithmetic/__init__.py new file mode 100644 index 0000000..6bc561b --- /dev/null +++ b/oraqle/compiler/arithmetic/__init__.py @@ -0,0 +1 @@ +"""This module contains classes for arithmetic operations that are not simply additions or multiplications.""" diff --git a/oraqle/compiler/arithmetic/exponentiation.py b/oraqle/compiler/arithmetic/exponentiation.py new file mode 100644 index 0000000..44e8ec0 --- /dev/null +++ b/oraqle/compiler/arithmetic/exponentiation.py @@ -0,0 +1,109 @@ +"""This module contains classes and functions for efficient exponentiation circuits.""" +import math +from typing import Type + +from galois import GF, FieldArray + +from oraqle.add_chains.addition_chains_front import gen_pareto_front +from oraqle.add_chains.addition_chains_heuristic import add_chain_guaranteed +from oraqle.add_chains.solving import extract_indices +from oraqle.compiler.nodes.abstract import CostParetoFront, Node +from oraqle.compiler.nodes.binary_arithmetic import Multiplication +from oraqle.compiler.nodes.leafs import Input +from oraqle.compiler.nodes.univariate import UnivariateNode + + +# TODO: Think about the role of Power when there are also Products +class Power(UnivariateNode): + """Represents an exponentiation: x ** constant.""" + + @property + def _node_shape(self) -> str: + return "box" + + @property + def _hash_name(self) -> str: + return f"pow_{self._exponent}" + + @property + def _node_label(self) -> str: + return f"Pow: {self._exponent}" + + def __init__(self, node: Node, exponent: int, gf: Type[FieldArray]): + """Initialize a `Power` node that exponentiates `node` with `exponent`.""" + self._exponent = exponent + super().__init__(node, gf) + + def _operation_inner(self, input: FieldArray, gf: Type[FieldArray]) -> FieldArray: + return input**self._exponent # type: ignore + + def _arithmetize_inner(self, strategy: str) -> "Node": + if strategy == "naive": + # Square & multiply + nodes = [self._node.arithmetize(strategy)] + + for i in range(math.ceil(math.log2(self._exponent))): + nodes.append(nodes[i].mul(nodes[i], flatten=False)) + previous = None + for i in range(math.ceil(math.log2(self._exponent))): + if (self._exponent >> i) & 1: + if previous is None: + previous = nodes[i] + else: + nodes.append(nodes[i].mul(previous, flatten=False)) + previous = nodes[-1] + + assert previous is not None + return previous + + assert strategy == "best-effort" + + addition_chain = add_chain_guaranteed(self._exponent, self._gf.characteristic - 1, squaring_cost=1.0) + + nodes = [self._node.arithmetize(strategy).to_arithmetic()] + + for i, j in addition_chain: + nodes.append(Multiplication(nodes[i], nodes[j], self._gf)) + + return nodes[-1] + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + # TODO: While generating the front, we can take into account the maximum cost etc. implied by the depth-aware arithmetization of the operand + if self._gf.characteristic <= 257: + front = gen_pareto_front(self._exponent, self._gf.characteristic, cost_of_squaring) + else: + front = gen_pareto_front(self._exponent, None, cost_of_squaring) + + final_front = CostParetoFront(cost_of_squaring) + + for depth1, _, node in self._node.arithmetize_depth_aware(cost_of_squaring): + for depth2, chain in front: + c = extract_indices( + chain, + modulus=self._gf.characteristic - 1 if self._gf.characteristic <= 257 else None, + ) + + nodes = [node] + + for i, j in c: + nodes.append(Multiplication(nodes[i], nodes[j], self._gf)) + + final_front.add(nodes[-1], depth=depth1 + depth2) + + return final_front + + +def test_depth_aware_arithmetization(): # noqa: D103 + gf = GF(31) + + x = Input("x", gf) + node = Power(x, 30, gf) + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + node.clear_cache(set()) + + for _, _, n in front: + assert n.evaluate({"x": gf(0)}) == 0 + n.clear_cache(set()) + + for xx in range(1, 31): + assert n.evaluate({"x": gf(xx)}) == 1 diff --git a/oraqle/compiler/arithmetic/subtraction.py b/oraqle/compiler/arithmetic/subtraction.py new file mode 100644 index 0000000..bda0437 --- /dev/null +++ b/oraqle/compiler/arithmetic/subtraction.py @@ -0,0 +1,68 @@ +"""This module contains classes for representing subtraction: x - y.""" +from galois import GF, FieldArray + +from oraqle.compiler.nodes.abstract import CostParetoFront, Node +from oraqle.compiler.nodes.leafs import Constant, Input +from oraqle.compiler.nodes.non_commutative import NonCommutativeBinaryNode + + +class Subtraction(NonCommutativeBinaryNode): + """Represents a subtraction, which can be arithmetized using addition and constant-multiplication.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"shape": "square", "style": "rounded,filled", "fillcolor": "cornsilk"} + + @property + def _hash_name(self) -> str: + return "sub" + + @property + def _node_label(self) -> str: + return "-" + + def _operation_inner(self, x, y) -> FieldArray: + return x - y + + def _arithmetize_inner(self, strategy: str) -> Node: + # TODO: Reorganize the files: let the arithmetic folder only contain pure arithmetic (including add and mul) and move exponentiation elsewhere. + # TODO: For schemes that support subtraction we do not need to do this. We should only do this transformation during the compiler stage. + return (self._left.arithmetize(strategy) + (Constant(-self._gf(1)) * self._right.arithmetize(strategy))).arithmetize(strategy) # type: ignore # TODO: Should we always perform a final arithmetization in every node for constant folding? E.g. in Node? + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + result = self._left + (Constant(-self._gf(1)) * self._right) + front = result.arithmetize_depth_aware(cost_of_squaring) + return front + + +def test_evaluate_mod5(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Subtraction(a, b, gf) + + assert node.evaluate({"a": gf(3), "b": gf(2)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(4), "b": gf(1)}) == gf(3) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(3)}) == gf(3) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(4)}) == gf(1) + + +def test_evaluate_arithmetized_mod5(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Subtraction(a, b, gf).arithmetize("best-effort") + node.clear_cache(set()) + + assert node.evaluate({"a": gf(3), "b": gf(2)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(4), "b": gf(1)}) == gf(3) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(3)}) == gf(3) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(4)}) == gf(1) diff --git a/oraqle/compiler/boolean/__init__.py b/oraqle/compiler/boolean/__init__.py new file mode 100644 index 0000000..e68ad67 --- /dev/null +++ b/oraqle/compiler/boolean/__init__.py @@ -0,0 +1 @@ +"""This package contains nodes for expressing common Boolean operations.""" diff --git a/oraqle/compiler/boolean/bool_and.py b/oraqle/compiler/boolean/bool_and.py new file mode 100644 index 0000000..c172567 --- /dev/null +++ b/oraqle/compiler/boolean/bool_and.py @@ -0,0 +1,743 @@ +"""This module contains tools for evaluating AND operations between many inputs.""" +import itertools +import math +from abc import ABC, abstractmethod +from collections import Counter +from heapq import heapify, heappop, heappush +from typing import Iterable, List, Optional, Sequence, Set, Tuple, Type + +from galois import GF, FieldArray + +from oraqle.add_chains.addition_chains_front import gen_pareto_front +from oraqle.add_chains.addition_chains_mod import chain_cost +from oraqle.add_chains.solving import extract_indices +from oraqle.compiler.boolean.bool_neg import Neg +from oraqle.compiler.comparison.equality import IsNonZero +from oraqle.compiler.nodes.abstract import ( + ArithmeticNode, + CostParetoFront, + Node, + UnoverloadedWrapper, +) +from oraqle.compiler.nodes.arbitrary_arithmetic import ( + _PrioritizedItem, + Product, + Sum, + _generate_multiplication_tree, +) +from oraqle.compiler.nodes.binary_arithmetic import Multiplication +from oraqle.compiler.nodes.flexible import CommutativeUniqueReducibleNode +from oraqle.compiler.nodes.leafs import Constant, Input + + +class And(CommutativeUniqueReducibleNode): + """Performs an AND operation over several operands. The user must ensure that the operands are Booleans.""" + + @property + def _hash_name(self) -> str: + return "and" + + @property + def _node_label(self) -> str: + return "AND" + + def _inner_operation(self, a: FieldArray, b: FieldArray) -> FieldArray: + return self._gf(bool(a) & bool(b)) + + def _arithmetize_inner(self, strategy: str) -> Node: # noqa: PLR0911, PLR0912 + new_operands: Set[UnoverloadedWrapper] = set() + for operand in self._operands: + new_operand = operand.node.arithmetize(strategy) + + if isinstance(new_operand, Constant): + if not bool(new_operand._value): + return Constant(self._gf(0)) + continue + + new_operands.add(UnoverloadedWrapper(new_operand)) + + if len(new_operands) == 0: + return Constant(self._gf(1)) + elif len(new_operands) == 1: + return next(iter(new_operands)).node + + if strategy == "naive": + return Product(Counter({operand: 1 for operand in new_operands}), self._gf).arithmetize( + strategy + ) + + # TODO: Calling to_arithmetic here should not be necessary if we can decide the predicted depth + queue = [ + ( + _PrioritizedItem( + 0, operand.node + ) # TODO: We should just maybe make a breadth method on Node + if isinstance(operand.node, Constant) + else _PrioritizedItem( + operand.node.to_arithmetic().multiplicative_depth(), operand.node + ) + ) + for operand in new_operands + ] + heapify(queue) + + while len(queue) > (self._gf._characteristic - 1): + total_sum = None + max_depth = None + for _ in range(self._gf._characteristic - 1): + if len(queue) == 0: + break + + popped = heappop(queue) + if max_depth is None or max_depth < popped.priority: + max_depth = popped.priority + + if total_sum is None: + total_sum = Neg(popped.item, self._gf) + else: + total_sum += Neg(popped.item, self._gf) + + assert total_sum is not None + final_result = Neg(IsNonZero(total_sum, self._gf), self._gf).arithmetize(strategy) + + assert max_depth is not None + heappush(queue, _PrioritizedItem(max_depth, final_result)) + + if len(queue) == 1: + return heappop(queue).item + + dummy_node = Input("dummy_node", self._gf) + is_non_zero = IsNonZero(dummy_node, self._gf).arithmetize(strategy).to_arithmetic() + cost = is_non_zero.multiplicative_cost( + 1.0 + ) # FIXME: This needs to be the actual squaring cost + + if len(queue) - 1 < cost: + return Product( + Counter({UnoverloadedWrapper(operand.item): 1 for operand in queue}), self._gf + ).arithmetize(strategy) + + return Neg( + IsNonZero( + Sum( + Counter({UnoverloadedWrapper(Neg(node.item, self._gf)): 1 for node in queue}), + self._gf, + ), + self._gf, + ), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + new_operands: Set[CostParetoFront] = set() + for operand in self._operands: + new_operand = operand.node.arithmetize_depth_aware(cost_of_squaring) + new_operands.add(new_operand) + + if len(new_operands) == 0: + return CostParetoFront.from_leaf(Constant(self._gf(1)), cost_of_squaring) + elif len(new_operands) == 1: + return next(iter(new_operands)) + + front = CostParetoFront(cost_of_squaring) + + # TODO: This is brute force composition + for operands in itertools.product(*(iter(new_operand) for new_operand in new_operands)): + checked_operands = [] + for depth, cost, node in operands: + if isinstance(node, Constant): + assert int(node._value) in {0, 1} + if node._value == 0: + return CostParetoFront.from_leaf(Constant(self._gf(0)), cost_of_squaring) + else: + checked_operands.append((depth, cost, node)) + + if len(checked_operands) == 0: + return CostParetoFront.from_leaf(Constant(self._gf(1)), cost_of_squaring) + + if len(checked_operands) == 1: + depth, cost, node = checked_operands[0] + front.add(node, depth, cost) + continue + + this_front = _find_depth_cost_front( + checked_operands, + self._gf, + float("inf"), + squaring_cost=cost_of_squaring, + is_and=True, + ) + front.add_front(this_front) + + return front + + def and_flatten(self, other: Node) -> Node: + """Performs an AND operation with `other`, flattening the `And` node if either of the two is also an `And` and absorbing `Constant`s. + + Returns: + An `And` node containing the flattened AND operation, or a `Constant` node. + """ + if isinstance(other, Constant): + if bool(other._value): + return self + else: + return Constant(self._gf(0)) + + if isinstance(other, And): + return And(self._operands | other._operands, self._gf) + + new_operands = self._operands.copy() + new_operands.add(UnoverloadedWrapper(other)) + return And(new_operands, self._gf) + + +def test_evaluate_mod3(): # noqa: D103 + gf = GF(3) + + a = Input("a", gf) + b = Input("b", gf) + node = (a & b).arithmetize("best-effort") + + assert node.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(1)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(0)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_mod3(): # noqa: D103 + gf = GF(3) + + a = Input("a", gf) + b = Input("b", gf) + node = (a & b).arithmetize("best-effort") + + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(1)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(0)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_depth_aware_mod2(): # noqa: D103 + gf = GF(2) + + a = Input("a", gf) + b = Input("b", gf) + node = a & b + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, n in front: + n.clear_cache(set()) + assert n.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(0), "b": gf(1)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(1), "b": gf(0)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_depth_aware_mod3(): # noqa: D103 + gf = GF(3) + + a = Input("a", gf) + b = Input("b", gf) + node = a & b + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, n in front: + n.clear_cache(set()) + assert n.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(0), "b": gf(1)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(1), "b": gf(0)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_depth_aware_7_mod5(): # noqa: D103 + gf = GF(5) + + xs = {Input(f"x{i}", gf) for i in range(7)} + node = And({UnoverloadedWrapper(x) for x in xs}, gf) # type: ignore + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, n in front: + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(0) for i in range(50)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(i % 2) for i in range(50)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(1) for i in range(50)}) == gf(1) + + +def test_evaluate_arithmetized_depth_aware_50_mod31(): # noqa: D103 + gf = GF(31) + + xs = {Input(f"x{i}", gf) for i in range(50)} + node = And({UnoverloadedWrapper(x) for x in xs}, gf) # type: ignore + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, n in front: + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(0) for i in range(50)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(i % 2) for i in range(50)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(1) for i in range(50)}) == gf(1) + + +class NaryLogicNode(ABC): + """Represents a (sub)circuit for computing and AND or OR operation.""" + + def __init__(self, breadth: int, cost: float) -> None: + """Initialize this logic node with the given `breadth` and `cost` (which are not checked).""" + self.breadth = breadth + self.cost = cost + + @abstractmethod + def local_cost(self) -> float: + """Compute the local multiplicative cost, so ignoring the cost of the inputs.""" + + @abstractmethod + def print(self, level: int = 0): + """Prints this subcircuit for debugging purposes.""" + + @abstractmethod + def to_arithmetic_node(self, is_and: bool, gf: Type[FieldArray]) -> ArithmeticNode: + """Returns an `ArithmeticNode` representing this logic node (AND if `is_and = True` else OR).""" + + +class InputNaryLogicNode(NaryLogicNode): + """An input logic node.""" + + def __init__(self, node: ArithmeticNode, breadth: int) -> None: + """Initialize the input node with the given `breadth`.""" + self._node = node + super().__init__(breadth, 0.0) + + def local_cost(self) -> float: # noqa: D102 + return 0.0 + + def print(self, level: int = 0): # noqa: D102 + print(" " * level + "x") + + def to_arithmetic_node(self, is_and: bool, gf: Type[FieldArray]) -> ArithmeticNode: # noqa: D102 + return self._node + + +class ProductNaryLogicNode(NaryLogicNode): + """A `ProductNaryLogicNode` represents an OR/AND (sub)circuit in which all inputs are multiplied (and flattened).""" + + def __init__(self, operands: List[NaryLogicNode], breadth: int) -> None: + """Initialize a product subcircuit with the given `operands` and `breadth`.""" + # Merge subproducts into this product + self._operands = list( + itertools.chain.from_iterable( + operand._operands if isinstance(operand, ProductNaryLogicNode) else [operand] + for operand in operands + ) + ) + self._arithmetic_node = None + self._is_and = None + super().__init__(breadth, self._compute_cost()) + + def _compute_cost(self) -> float: + return sum(op.cost for op in self._operands) + len(self._operands) - 1 + + def local_cost(self) -> float: # noqa: D102 + return len(self._operands) - 1 + + def print(self, level: int = 0): # noqa: D102 + print(" " * level + "prod:") + for op in self._operands: + op.print(level + 1) + + def to_arithmetic_node(self, is_and: bool, gf: Type[FieldArray]) -> ArithmeticNode: # noqa: D102 + if self._is_and is not None and self._is_and != is_and: + self._arithmetic_node = None + + if self._arithmetic_node is None: + _, result = _generate_multiplication_tree(((math.ceil(math.log2(operand.breadth)), operand.to_arithmetic_node(is_and, gf) if is_and else Neg(operand.to_arithmetic_node(is_and, gf), gf).arithmetize("best-effort").to_arithmetic()) for operand in self._operands), (1 for _ in range(len(self._operands)))) # type: ignore + + if not is_and: + result = Neg(result, gf) + + self._arithmetic_node = result.arithmetize( + "best-effort" + ).to_arithmetic() # TODO: This could be more elegant + self._is_and = is_and + + assert math.ceil(math.log2(self.breadth)) == self._arithmetic_node.multiplicative_depth() # type: ignore + return self._arithmetic_node + + +class SumReduceNaryLogicNode(NaryLogicNode): + """A `SumReduceNaryLogicNode` represents an OR/AND (sub)circuit in which all inputs are summed and then reduced to a Boolean.""" + + def __init__( + self, + operands: List[NaryLogicNode], + exponentiation_depth: int, + exponentiation_cost: float, + exponentiation_chain: List[Tuple[int, int]], + breadth: int, + ) -> None: + """Initialize a sum-reduce subcircuit with the given exponentiation chain (and properties), over the given `operands`.""" + self._operands = operands + self._exponentiation_depth = exponentiation_depth + self._exponentiation_cost = exponentiation_cost + self._exponentiation_chain = exponentiation_chain + self._arithmetic_node = None + self._is_and = None + super().__init__(breadth, self._compute_cost()) + + def _compute_cost(self) -> float: + return sum(op.cost for op in self._operands) + self._exponentiation_cost + + def local_cost(self) -> float: # noqa: D102 + return self._exponentiation_cost + + def print(self, level: int = 0): # noqa: D102 + print(" " * level + f"sumred({self._exponentiation_depth}, {self._exponentiation_cost}):") + for op in self._operands: + op.print(level + 1) + + def to_arithmetic_node(self, is_and: bool, gf: Type[FieldArray]) -> ArithmeticNode: # noqa: D102 + if self._is_and is not None and self._is_and != is_and: + self._arithmetic_node = None + + if self._arithmetic_node is None: + # TODO: This should be replaced by augmented circuit nodes + if is_and: + result = ( + Sum( + Counter( + { + UnoverloadedWrapper( + Neg(operand.to_arithmetic_node(is_and, gf), gf) + ): 1 + for operand in self._operands + } + ), + gf, + ) + .arithmetize("best-effort") + .to_arithmetic() + ) + else: + result = ( + Sum( + Counter( + { + UnoverloadedWrapper(operand.to_arithmetic_node(is_and, gf)): 1 + for operand in self._operands + } + ), + gf, + ) + .arithmetize("best-effort") + .to_arithmetic() + ) + + # Exponentiation + chain = extract_indices(self._exponentiation_chain, modulus=gf.characteristic - 1) + nodes = [result] + for i, j in chain: + nodes.append(Multiplication(nodes[i], nodes[j], gf)) # type: ignore + result = nodes[-1] + + if is_and: + result = Neg(result, gf).arithmetize("best-effort") + + self._arithmetic_node = result.to_arithmetic() # TODO: This could be more elegant + self._is_and = is_and + + assert math.ceil(math.log2(self.breadth)) == self._arithmetic_node.multiplicative_depth() # type: ignore + return self._arithmetic_node + + +def _minimum_cost(operand_count: int, exponentiation_cost: float, p: int) -> float: + r = math.ceil((p - 1 - operand_count) / (2 - p)) + return r * exponentiation_cost + min(exponentiation_cost, operand_count + r * (2 - p) - 1) + + +def _find_depth_cost_front( + operands: Sequence[Tuple[int, float, ArithmeticNode]], + gf: Type[FieldArray], + strict_cost_upper: float, + squaring_cost: float, + is_and: bool, +) -> CostParetoFront: + new_operands: List[NaryLogicNode] = [ + InputNaryLogicNode(node, 0 if isinstance(node, Constant) else 2**depth) + for depth, _, node in operands + ] + + circuits = minimize_depth_cost( + new_operands, gf.characteristic, strict_cost_upper, squaring_cost + ) + + front = CostParetoFront(squaring_cost) + for depth, _, node in circuits: + front.add(node.to_arithmetic_node(is_and, gf), depth) + + return front + + +# TODO: This is copied from arbitrary_arithmetic.py +def _generate_sumred_tree( + operands: Iterable[Tuple[int, InputNaryLogicNode]], + squaring_cost: float, +) -> Tuple[int, SumReduceNaryLogicNode]: + queue = [_PrioritizedItem(*operand) for operand in operands] + heapify(queue) + + while len(queue) > 1: + a = heappop(queue) + b = heappop(queue) + + depth = max(a.priority, b.priority) + 1 + heappush( + queue, + _PrioritizedItem( + depth, + SumReduceNaryLogicNode([a.item, b.item], 2, squaring_cost, [(1, 1)], 2**depth), + ), + ) + + return (queue[0].priority, queue[0].item) + + +def minimize_depth_cost( + operands: List[NaryLogicNode], p: int, strict_cost_upper: float, squaring_cost: float +) -> List[Tuple[int, float, NaryLogicNode]]: + """Finds the depth-cost Pareto front. + + Returns: + A front in the form of a list of tuples containing (depth, cost, node). + """ + assert len(operands) >= 2 + + if p == 2: + result = ProductNaryLogicNode( + operands, breadth=sum(operand.breadth for operand in operands) + ) + return [(math.ceil(math.log2(result.breadth)), result.cost, result)] + + if p == 3: + depth, result = _generate_sumred_tree([(math.ceil(math.log2(operand.breadth)), operand) for operand in operands], squaring_cost) # type: ignore + return [(depth, result.cost, result)] + + sorted_operands = sorted(operands, key=lambda op: op.breadth, reverse=True) + depth_limit = math.ceil(math.log2(sorted_operands[0].breadth)) # + 1 + + front = gen_pareto_front(p - 1, p, squaring_cost) + exponentiation_specs = [ + (depth, chain_cost(chain, squaring_cost), chain) for depth, chain in front + ] + _, cheapest_exponentiation_cost, _ = exponentiation_specs[-1] + + mincost = _minimum_cost(len(sorted_operands), cheapest_exponentiation_cost, p) + + circuits = [] + while True: + breadth_limit = 2**depth_limit + result = minimize_depth_cost_recursive( + sorted_operands, breadth_limit, exponentiation_specs, p, strict_cost_upper + ) + + if result is None: + depth_limit += 1 + continue + + assert result.cost >= mincost + assert result.cost < strict_cost_upper, f"{result.cost} >= {strict_cost_upper}" + + if result.cost == mincost: + circuits.append((depth_limit, result.cost, result)) + return circuits + + circuits.append((depth_limit, result.cost, result)) + strict_cost_upper = result.cost + + # TODO: If we want to return the minimum breadth we have to increment at a higher resolution + depth_limit += 1 + + +def _find_index_breadth(sorted_operands: List[NaryLogicNode], greater_or_equal_to: int) -> int: + for i in range(len(sorted_operands)): + if sorted_operands[i].breadth < greater_or_equal_to: + return i + + return len(sorted_operands) + + +def _insert(sorted_operands: List[NaryLogicNode], node: NaryLogicNode): + for i in range(len(sorted_operands)): + if sorted_operands[i].breadth < node.breadth: + sorted_operands.insert(i, node) + return + + sorted_operands.append(node) + + +def minimize_depth_cost_recursive( # noqa: PLR0912, PLR0914, PLR0915 + sorted_operands: List[NaryLogicNode], + breadth_limit: int, + exponentiation_specs: List[Tuple[int, float, List[Tuple[int, int]]]], + p: int, + strict_cost_upper: float, +) -> Optional[NaryLogicNode]: + """Find a minimum-depth circuit for the given `breadth_limit` and `strict_cost_upper` bound. + + Operands must be sorted from deep to shallow. + Returns the lowest-cost circuit for the given depth. + The exponentiation_specs must be sorted from high-cost to low-cost. + + Returns: + A minimum-depth circuit in the form of an `NaryLogicNode` satisfying the constraints, or None if the constraints cannot be satisfied. + """ + if len(sorted_operands) == 1: + if breadth_limit >= sorted_operands[0].breadth and strict_cost_upper > 0: + assert sorted_operands[0].cost < strict_cost_upper + return sorted_operands[0] + return None + + # If the breadth limit is exceeded, stop + if breadth_limit < 1: + return None + + # If the cost limit is exceeded, stop + if strict_cost_upper <= 0: + return None + + # If the lower bound for the cost exceeds the limit, also stop + _, cheapest_exponentiation_cost, _ = exponentiation_specs[-1] + lower_bound_cost = _minimum_cost(len(sorted_operands), cheapest_exponentiation_cost, p) + if lower_bound_cost >= strict_cost_upper: + return None + + output = None + + for exponentiation_depth, exponentiation_cost, exponentiation_chain in exponentiation_specs: + # We do not call .cost() in this algorithm because we only consider the cost of the AND/OR subcircuit + + type_2_limit = 2 ** (math.ceil(math.log2(breadth_limit)) - exponentiation_depth) + if len(sorted_operands) < p: + if ( + all(operand.breadth <= type_2_limit for operand in sorted_operands) + and exponentiation_cost < strict_cost_upper + ): + # Use a type-2 arithmetization + depth = math.ceil(math.log2(sorted_operands[0].breadth)) + exponentiation_depth + output = SumReduceNaryLogicNode( + sorted_operands, + exponentiation_depth, + exponentiation_cost, + exponentiation_chain, + breadth=2**depth, + ) + strict_cost_upper = exponentiation_cost + + if (tot := sum(op.breadth for op in sorted_operands)) <= breadth_limit and len( + sorted_operands + ) - 1 < strict_cost_upper: + output = ProductNaryLogicNode(sorted_operands, breadth=tot) + strict_cost_upper = len(sorted_operands) - 1 + + continue + + # At this point, we know that len(sorted_operands) >= p + + # Try a type-1 arithmetization, so no type-2 at all + if (tot := sum(op.breadth for op in sorted_operands)) <= breadth_limit and len( + sorted_operands + ) - 1 < strict_cost_upper: + output = ProductNaryLogicNode(sorted_operands, breadth=tot) + strict_cost_upper = len(sorted_operands) - 1 + + reduced = all(operand.breadth <= type_2_limit for operand in sorted_operands) + if reduced: + if exponentiation_cost >= strict_cost_upper: + continue + + # Use a type-2 arithmetization on operands of decreasing depth + cache = set() + for i in range(len(sorted_operands) - 1): + selected_operands = sorted_operands[i : (i + p - 1)] + breadths = tuple(operand.breadth for operand in selected_operands) + if breadths in cache: + continue + cache.add(breadths) + + depth = math.ceil(math.log2(selected_operands[0].breadth)) + exponentiation_depth + + new_operands = sorted_operands[:i] + if i + p - 1 < len(sorted_operands): + new_operands += sorted_operands[i + p - 1 :] + + breadth = 2**depth + sum_red = SumReduceNaryLogicNode( + sorted_operands[i : i + p - 1], + exponentiation_depth, + exponentiation_cost, + exponentiation_chain, + breadth=breadth, + ) + _insert(new_operands, sum_red) + + potential_output = minimize_depth_cost_recursive( + new_operands, + breadth_limit, + exponentiation_specs, + p, + strict_cost_upper - exponentiation_cost, + ) + if potential_output is not None: + output = potential_output + strict_cost_upper -= potential_output.local_cost() + else: + # Isolate all the operands that cannot use a type-2 arithmetization + first_small_index = _find_index_breadth(sorted_operands, type_2_limit) + large_operands = sorted_operands[:first_small_index] + small_operands = sorted_operands[first_small_index:] + + # If there are no small operands, then this arithmetization is not possible + if len(small_operands) == 0: + continue + + # Use a type-1 arithmetization for large_operands + assert len(large_operands) > 0 + cost = len( + large_operands + ) # Not -1 because we also need a multiplication with the AND/OR of small_operands + if cost >= strict_cost_upper: + continue + + breadth = sum(operand.breadth for operand in large_operands) + new_breadth_limit = breadth_limit - breadth + + sub_output = minimize_depth_cost_recursive( + small_operands, new_breadth_limit, exponentiation_specs, p, strict_cost_upper - cost + ) + if sub_output is not None: + output = ProductNaryLogicNode( + [*large_operands, sub_output], breadth=breadth + sub_output.breadth + ) + strict_cost_upper -= output.local_cost() + + return output + + +def all_(*operands: Node) -> And: + """Returns an `And` node that evaluates to true if any of the given `operands` evaluates to true.""" + assert len(operands) > 0 + return And(set(UnoverloadedWrapper(operand) for operand in operands), operands[0]._gf) diff --git a/oraqle/compiler/boolean/bool_neg.py b/oraqle/compiler/boolean/bool_neg.py new file mode 100644 index 0000000..ac9ede5 --- /dev/null +++ b/oraqle/compiler/boolean/bool_neg.py @@ -0,0 +1,37 @@ +"""Classes for describing Boolean negation.""" +from galois import FieldArray + +from oraqle.compiler.arithmetic.subtraction import Subtraction +from oraqle.compiler.nodes.abstract import CostParetoFront, Node +from oraqle.compiler.nodes.leafs import Constant +from oraqle.compiler.nodes.univariate import UnivariateNode + + +class Neg(UnivariateNode): + """A node that negates a Boolean input.""" + + @property + def _node_shape(self) -> str: + return "box" + + @property + def _hash_name(self) -> str: + return "neg" + + @property + def _node_label(self) -> str: + return "NEG" + + def _operation_inner(self, input: FieldArray) -> FieldArray: + assert input in {0, 1} + return self._gf(not bool(input)) + + def _arithmetize_inner(self, strategy: str) -> Node: + return Subtraction( + Constant(self._gf(1)), self._node.arithmetize(strategy), self._gf + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return Subtraction(Constant(self._gf(1)), self._node, self._gf).arithmetize_depth_aware( + cost_of_squaring + ) diff --git a/oraqle/compiler/boolean/bool_or.py b/oraqle/compiler/boolean/bool_or.py new file mode 100644 index 0000000..eb92a12 --- /dev/null +++ b/oraqle/compiler/boolean/bool_or.py @@ -0,0 +1,181 @@ +"""This module contains tools for evaluating OR operations between many inputs.""" +import itertools +from typing import Set + +from galois import GF, FieldArray + +from oraqle.compiler.boolean.bool_and import And, _find_depth_cost_front +from oraqle.compiler.boolean.bool_neg import Neg +from oraqle.compiler.nodes.abstract import CostParetoFront, Node, UnoverloadedWrapper +from oraqle.compiler.nodes.flexible import CommutativeUniqueReducibleNode +from oraqle.compiler.nodes.leafs import Constant, Input + +# TODO: Reduce code duplication between OR and AND + + +class Or(CommutativeUniqueReducibleNode): + """Performs an OR operation over several operands. The user must ensure that the operands are Booleans.""" + + @property + def _hash_name(self) -> str: + return "or" + + @property + def _node_label(self) -> str: + return "OR" + + def _inner_operation(self, a: FieldArray, b: FieldArray) -> FieldArray: + return self._gf(bool(a) | bool(b)) + + def _arithmetize_inner(self, strategy: str) -> Node: + # FIXME: Handle what happens when arithmetize outputs a constant! + # TODO: Also consider the arithmetization using randomness + return Neg( + And( + { + UnoverloadedWrapper(Neg(operand.node.arithmetize(strategy), self._gf)) + for operand in self._operands + }, + self._gf, + ), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + # TODO: This is mostly copied from AND + new_operands: Set[CostParetoFront] = set() + for operand in self._operands: + new_operand = operand.node.arithmetize_depth_aware(cost_of_squaring) + new_operands.add(new_operand) + + if len(new_operands) == 0: + return CostParetoFront.from_leaf(Constant(self._gf(1)), cost_of_squaring) + elif len(new_operands) == 1: + return next(iter(new_operands)) + + # TODO: We can check if any of the element in new_operands are constants and return early + + front = CostParetoFront(cost_of_squaring) + + # TODO: This is brute force composition + for operands in itertools.product(*(iter(new_operand) for new_operand in new_operands)): + checked_operands = [] + for depth, cost, node in operands: + if isinstance(node, Constant): + assert node._value in {0, 1} + if node._value == 0: + return CostParetoFront.from_leaf(Constant(self._gf(0)), cost_of_squaring) + else: + checked_operands.append((depth, cost, node)) + + if len(checked_operands) == 0: + return CostParetoFront.from_leaf(Constant(self._gf(1)), cost_of_squaring) + + if len(checked_operands) == 1: + depth, cost, node = checked_operands[0] + front.add(node, depth, cost) + continue + + this_front = _find_depth_cost_front( + checked_operands, + self._gf, + float("inf"), + squaring_cost=cost_of_squaring, + is_and=False, + ) + front.add_front(this_front) + + return front + + def or_flatten(self, other: Node) -> Node: + """Performs an OR operation with `other`, flattening the `Or` node if either of the two is also an `Or` and absorbing `Constant`s. + + Returns: + An `Or` node containing the flattened OR operation, or a `Constant` node. + """ + if isinstance(other, Constant): + if bool(other._value): + return Constant(self._gf(1)) + else: + return self + + if isinstance(other, Or): + return Or(self._operands | other._operands, self._gf) + + new_operands = self._operands.copy() + new_operands.add(UnoverloadedWrapper(other)) + return Or(new_operands, self._gf) + + +def any_(*operands: Node) -> Or: + """Returns an `Or` node that evaluates to true if any of the given `operands` evaluates to true.""" + assert len(operands) > 0 + return Or(set(UnoverloadedWrapper(operand) for operand in operands), operands[0]._gf) + + +def test_evaluate_mod3(): # noqa: D103 + gf = GF(3) + + a = Input("a", gf) + b = Input("b", gf) + node = a | b + + assert node.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(1)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(0)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_depth_aware_mod2(): # noqa: D103 + gf = GF(2) + + a = Input("a", gf) + b = Input("b", gf) + node = a | b + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, n in front: + n.clear_cache(set()) + assert n.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({"a": gf(0), "b": gf(1)}) == gf(1) + n.clear_cache(set()) + assert n.evaluate({"a": gf(1), "b": gf(0)}) == gf(1) + n.clear_cache(set()) + assert n.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_mod3(): # noqa: D103 + gf = GF(3) + + a = Input("a", gf) + b = Input("b", gf) + node = (a | b).arithmetize("best-effort") + + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(0)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(1)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(0)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(1)}) == gf(1) + + +def test_evaluate_arithmetized_depth_aware_50_mod31(): # noqa: D103 + gf = GF(31) + + xs = {Input(f"x{i}", gf) for i in range(50)} + node = Or({UnoverloadedWrapper(x) for x in xs}, gf) + front = node.arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, n in front: + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(0) for i in range(50)}) == gf(0) + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(i % 2) for i in range(50)}) == gf(1) + n.clear_cache(set()) + assert n.evaluate({f"x{i}": gf(1) for i in range(50)}) == gf(1) diff --git a/oraqle/compiler/circuit.py b/oraqle/compiler/circuit.py new file mode 100644 index 0000000..6dd194b --- /dev/null +++ b/oraqle/compiler/circuit.py @@ -0,0 +1,415 @@ +"""This module contains classes for representing circuits.""" +import subprocess +import tempfile +from typing import Dict, List, Optional, Tuple + +from fhegen.bgv import logqP +from fhegen.util import estsecurity +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.instructions import ArithmeticProgram, OutputInstruction +from oraqle.compiler.nodes.abstract import ArithmeticNode, Node + + +class Circuit: + """Represents a circuit over a fixed finite field that can be turned into an arithmetic circuit. Behind the scenes this is a directed acyclic graph (DAG). The circuit only has references to the outputs.""" + + def __init__(self, outputs: List[Node]): + """Initialize a circuit with the given `outputs`.""" + assert len(outputs) > 0 + self._outputs = outputs + self._gf = outputs[0]._gf + + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> List[FieldArray]: + """Evaluates the circuit with the given named inputs. + + This function does not error if it is given more inputs than necessary, but it will error if one is missing. + + Returns: + Evaluated output in plain text. + """ + assert all(isinstance(value, self._gf) for value in actual_inputs.values()) + + actual_outputs = [output.evaluate(actual_inputs) for output in self._outputs] + self._clear_cache() + + return actual_outputs + + def to_graph(self, file_name: str): + """Saves a DOT file representing the circuit as a graph at the given `file_name`.""" + graph_builder = DotFile() + + for output in self._outputs: + graph_builder.add_link( + output.to_graph(graph_builder), + graph_builder.add_node(label="Output", shape="plain"), + ) + self._clear_cache() + + graph_builder.to_file(file_name) + + def to_pdf(self, file_name: str): + """Saves a PDF file representing the circuit as a graph at the given `file_name`.""" + with tempfile.NamedTemporaryFile(suffix=".dot", delete=False) as dot_file: + self.to_graph(dot_file.name) + + subprocess.run(["dot", "-Tpdf", dot_file.name, "-o", file_name], check=True) + + def display_graph(self, metadata: Optional[dict] = None): + """Displays the circuit in a Python notebook.""" + with tempfile.NamedTemporaryFile(suffix=".dot", delete=False) as dot_file: + self.to_graph(dot_file.name) + + with open(dot_file.name, encoding="utf8") as file: + file_content = file.read() + + import graphviz + from IPython.display import display_png + + src = graphviz.Source(file_content) + display_png(src, metadata=metadata) + + def eliminate_subexpressions(self): + """Perform semantic common subexpression elimination on all outputs.""" + for output in self._outputs: + output.eliminate_common_subexpressions({}) + + def is_equivalent(self, other: object) -> bool: + """Returns whether the two circuits are semantically equivalent. + + False positives do not occure but false negatives do. + """ + if not isinstance(other, self.__class__): + return False + + return all(out1.is_equivalent(out2) for out1, out2 in zip(self._outputs, other._outputs)) + + def arithmetize(self, strategy: str = "best-effort") -> "ArithmeticCircuit": + """Arithmetizes this circuit by calling arithmetize on all outputs. + + This replaces all high-level operations with arithmetic operations (constants, additions, and multiplications). + The current implementation only aims at reducing the total number of multiplications. + + Returns: + An equivalent arithmetic circuit with low multiplicative size. + """ + arithmetic_circuit = ArithmeticCircuit( + [output.arithmetize(strategy).to_arithmetic() for output in self._outputs] + ) + # FIXME: Also call to_arithmetic + arithmetic_circuit._clear_cache() + + return arithmetic_circuit + + def arithmetize_depth_aware( + self, cost_of_squaring: float = 1.0 + ) -> List[Tuple[int, int, "ArithmeticCircuit"]]: + """Perform depth-aware arithmetization on this circuit. + + !!! failure + The current implementation only supports circuits with a single output. + + This function replaces high-level nodes with arithmetic operations (constants, additions, and multiplications). + + Returns: + A list with tuples containing the multiplicative depth, the multiplicative cost, and the generated arithmetization from low to high depth. + """ + assert len(self._outputs) == 1 + assert cost_of_squaring <= 1.0 + + front = [] + for depth, size, node in self._outputs[0].arithmetize_depth_aware(cost_of_squaring): + arithmetic_circuit = ArithmeticCircuit([node]) + arithmetic_circuit._clear_cache() + front.append((depth, size, arithmetic_circuit)) + + arithmetic_circuit._clear_cache() + return front + + def _clear_cache(self): + already_cleared = set() + for output in self._outputs: + output.clear_cache(already_cleared) + + +helib_preamble = """ +#include +#include +#include +#include + +#include + +typedef helib::Ptxt ptxt_t; +typedef helib::Ctxt ctxt_t; + +std::map input_map; + +void parse_arguments(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + std::string argument(argv[i]); + size_t pos = argument.find('='); + if (pos != std::string::npos) { + std::string key = argument.substr(0, pos); + int value = std::stoi(argument.substr(pos + 1)); + input_map[key] = value; + } + } +} + +int extract_input(const std::string& name) { + if (input_map.find(name) != input_map.end()) { + return input_map[name]; + } else { + std::cerr << "Error: " << name << " not found" << std::endl; + return -1; + } +} + +int main(int argc, char* argv[]) { + // Parse the inputs + parse_arguments(argc, argv); +""" + +helib_keygen = """ + // Generate keys + helib::SecKey secret_key(context); + secret_key.GenSecKey(); + helib::addSome1DMatrices(secret_key); + const helib::PubKey& public_key = secret_key; +""" + +helib_postamble = """ + return 0; +} +""" + + +class ArithmeticCircuit(Circuit): + """Represents an arithmetic circuit over a fixed finite field, so it only contains arithmetic nodes.""" + + _outputs: List[ArithmeticNode] + + def multiplicative_depth(self) -> int: + """Returns the multiplicative depth of the circuit.""" + depth = max(output.multiplicative_depth() for output in self._outputs) + self._clear_cache() + + return depth + + def multiplicative_size(self) -> int: + """Returns the multiplicative size (number of multiplications) of the circuit.""" + multiplications = set().union(*(output.multiplications() for output in self._outputs)) + size = len(multiplications) + + return size + + def multiplicative_cost(self, cost_of_squaring: float) -> float: + """Returns the multiplicative cost of the circuit.""" + multiplications = set().union(*(output.multiplications() for output in self._outputs)) + squarings = set().union(*(output.squarings() for output in self._outputs)) + cost = len(multiplications) - len(squarings) + cost_of_squaring * len(squarings) + + return cost + + def generate_program(self) -> ArithmeticProgram: + """Returns an arithmetic program for this arithmetic circuit.""" + # Reset the parent counts + for output in self._outputs: + output.reset_parent_count() + + # Count the parents + for output in self._outputs: + output.count_parents() + + # Reset the cache for instruction writing + self._clear_cache() + + # Write the instructions + instructions = [] + stack_occupied = [] + + stack_counter = 0 + for output in self._outputs: + output_index, stack_counter = output.create_instructions( + instructions, stack_counter, stack_occupied + ) + instructions.append(OutputInstruction(output_index)) + + # Reset the cache for future operations + self._clear_cache() + + return ArithmeticProgram(instructions, len(stack_occupied), self._gf) + + def summands_between_multiplications(self) -> int: + """Computes the maximum number of summands between two consecutive multiplications in this circuit. + + !!! failure + This currently returns the hardcoded value 10 + + Returns: + The highest number of summands between two consecutive multiplications + """ + # FIXME: This is currently hardcoded + return 10 + + def _generate_helib_params(self) -> Tuple[str, Tuple[int, int, int, int]]: + # Returns the code, along with (m, r, bits, c) + multiplicative_depth = self.multiplicative_depth() + summands_between_mults = self.summands_between_multiplications() + + # This code is adapted from fhegen: https://github.com/Crypto-TII/fhegen + # It was written by Johannes Mono, Chiara Marcolla, Georg Land, Tim Gรผneysu, and Najwa Aaraj + + ops = { + "model": "OpenFHE", + "muls": multiplicative_depth + 1, + "const": True, + "rots": 0, + "sums": summands_between_mults, + } + + sdist = "Ternary" + sigma = 3.19 + ve = sigma * sigma + vs = {"Ternary": 2 / 3, "Error": ve}[sdist] + b_args = { + "m": 4, + "t": self._gf.characteristic, + "D": 6, + "Vs": vs, + "Ve": ve, + } # We will loop over increasing m to find a suitable value + kswargs = {"method": "Hybrid-RNS", "L": multiplicative_depth + 1, "beta": 2**10, "omega": 3} + + while True: + logq, logp = logqP(ops, b_args, kswargs, sdist) + log = sum(logq) + logp if logp else sum(logq) + if logp and estsecurity(b_args["m"], log, sdist) >= 128: + break + + b_args["m"] <<= 1 + + # TODO: This is a workaround + if self._gf.characteristic == 2: + b_args["m"] -= 1 + + sec = estsecurity(b_args["m"], sum(logq) + logp, sdist) + assert sec >= 128 + + return f""" + // Set up the HE parameters + unsigned long p = {self._gf.characteristic}; + unsigned long m = {b_args["m"]}; + unsigned long r = 1; + unsigned long bits = {sum(logq)}; + unsigned long c = 3; + helib::Context context = helib::ContextBuilder() + .m(m) + .p(p) + .r(r) + .bits(bits) + .c(c) + .build(); +""", (b_args["m"], 1, sum(logq), 3) + + def generate_code( + self, + filename: str, + iterations: int = 1, + measure_time: bool = False, + decrypt_outputs: bool = False, + ) -> Tuple[int, int, int, int]: + """Generates an HElib implementation of the circuit. + + If decrypt_outputs is True, prints the decrypted output. + Otherwise, it prints whether the ciphertext has noise budget remaining (i.e. it is correct with high probability). + + !!! note + Decryption is part of the measured run time. + + Args: + filename: Test + iterations: Number of times to run the circuit + measure_time: Whether to output a measurement of the total run time + decrypt_outputs: Whether to print the decrypted outputs, or to simply check if there is noise budget remaining + + Returns: + Parameters that were chosen: (ring dimension m, Hensel lifting = 1, bits in the modchain, columns in key switching = 3). + """ + from oraqle.compiler.instructions import InputInstruction + + # Generate HElib code + with open(filename, "w", encoding="utf8") as file: + # Write start of file and parameters + file.write(helib_preamble) + param_code, params = self._generate_helib_params() + file.write(param_code) + file.write("\n") + file.write(helib_keygen) + file.write("\n") + + # Encrypt the inputs + program = self.generate_program() + inputs = [ + instruction._name + for instruction in program._instructions + if isinstance(instruction, InputInstruction) + ] + file.write("\t// Encrypt the inputs\n") + for input in inputs: + file.write( + f'\tstd::vector vec_{input}(1, extract_input("{input}"));\n\tptxt_t ptxt_{input}(context, vec_{input});\n\tctxt_t ciph_{input}(public_key);\n\tpublic_key.Encrypt(ciph_{input}, ptxt_{input});\n' + ) + file.write("\n") + + # If timing is enabled, start the timer + if measure_time: + file.write("\tauto start = std::chrono::high_resolution_clock::now();\n") + file.write("\n") + + # If we perform multiple iterations, wrap in a for loop + if iterations > 1: + file.write(f"\tfor (int i = 0; i < {iterations}; i++) {{\n") + + # Write the actual instructions + file.write("\t// Perform the actual circuit\n") + file.write( + "\n".join( + f"\t{line}" for line in program.generate_code(decrypt_outputs).splitlines() + ) + ) + file.write("\n") + + # If we perform multiple iterations, close the for loop + if iterations > 1: + file.write("\t}\n") + + # If timing is enabled, stop the timer + if measure_time: + file.write("\n") + file.write("\tauto end = std::chrono::high_resolution_clock::now();\n") + file.write("\tstd::chrono::duration elapsed = end - start;\n") + file.write("\tstd::cout << elapsed.count() << std::endl;") + file.write("\n") + + # Finish the file + file.write(helib_postamble) + + return params + + +if __name__ == "__main__": + from galois import GF + + from oraqle.compiler.circuit import Circuit + from oraqle.compiler.nodes.leafs import Input + + gf = GF(7) + + x = Input("x", gf) + y = Input("y", gf) + + arithmetic_circuit = Circuit([x < y]).arithmetize() + arithmetic_circuit.generate_code("main.cpp", iterations=10, measure_time=True) diff --git a/oraqle/compiler/comparison/__init__.py b/oraqle/compiler/comparison/__init__.py new file mode 100644 index 0000000..837510d --- /dev/null +++ b/oraqle/compiler/comparison/__init__.py @@ -0,0 +1 @@ +"""Package containing tools for expressing equality and comparison operations.""" diff --git a/oraqle/compiler/comparison/comparison.py b/oraqle/compiler/comparison/comparison.py new file mode 100644 index 0000000..18f3156 --- /dev/null +++ b/oraqle/compiler/comparison/comparison.py @@ -0,0 +1,693 @@ +"""Classes for representing comparisons such as x < y, x >= y, semi-comparisons etc.""" +from typing import Type + +from galois import GF, FieldArray + +from oraqle.compiler.arithmetic.subtraction import Subtraction +from oraqle.compiler.boolean.bool_neg import Neg +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.comparison.in_upper_half import IliashenkoZuccaInUpperHalf, InUpperHalf +from oraqle.compiler.nodes.abstract import CostParetoFront, Node, iterate_increasing_depth +from oraqle.compiler.nodes.leafs import Constant, Input +from oraqle.compiler.nodes.non_commutative import NonCommutativeBinaryNode + + +class AbstractComparison(NonCommutativeBinaryNode): + """An abstract class for comparisons, representing that they can be flipped: i.e. x > y <=> y < x.""" + + def __init__(self, left, right, less_than: bool, gf: Type[FieldArray]): + """Initialize an abstract comparison, indicating the direction of the comparison by specifying `less_than`.""" + self._less_than = less_than + super().__init__(left, right, gf) + + def __hash__(self) -> int: + if self._hash is None: + left_hash = hash(self._left) + right_hash = hash(self._right) + + if self._less_than: + self._hash = hash((self._hash_name, (left_hash, right_hash))) + else: + self._hash = hash((self._hash_name, (right_hash, left_hash))) + + return self._hash + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + if hash(self) != hash(other): + return False + + if self._less_than ^ other._less_than: + return self._left.is_equivalent(other._right) and self._right.is_equivalent(other._left) + else: + return self._left.is_equivalent(other._left) and self._right.is_equivalent(other._right) + + +class SemiStrictComparison(AbstractComparison): + """A node representing a comparison x < y or x > y that only works when x and y are at most p // 2 elements apart. + + Semi-comparisons are only valid if the absolute difference between the inputs does not exceed half of the field size. + """ + + @property + def _hash_name(self) -> str: + return "semi_strict_comparison" + + @property + def _node_label(self) -> str: + return "~<" if self._less_than else ">~" + + def _operation_inner(self, x, y) -> FieldArray: + assert abs(int(x) - int(y)) <= self._gf.characteristic // 2 + + if self._less_than: + return self._gf(int(int(x) < int(y))) + else: + return self._gf(int(int(x) > int(y))) + + def _arithmetize_inner(self, strategy: str) -> Node: + if self._less_than: + left = self._left + right = self._right + else: + left = self._right + right = self._left + + return InUpperHalf( + Subtraction(left.arithmetize(strategy), right.arithmetize(strategy), self._gf), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + front = CostParetoFront(cost_of_squaring) + + if self._less_than: + left = self._left + right = self._right + else: + left = self._right + right = self._left + + left_front = left.arithmetize_depth_aware(cost_of_squaring) + right_front = right.arithmetize_depth_aware(cost_of_squaring) + + for left, right in iterate_increasing_depth(left_front, right_front): + _, _, left_node = left + _, _, right_node = right + + sub_front = InUpperHalf( + Subtraction(left_node, right_node, self._gf), + self._gf, + ).arithmetize_depth_aware(cost_of_squaring) + + front.add_front(sub_front) + + assert not front.is_empty() + return front + + +class StrictComparison(AbstractComparison): + """A node representing a comparison x < y or x > y.""" + + @property + def _hash_name(self) -> str: + return "strict_comparison" + + @property + def _node_label(self) -> str: + return "<" + + def _operation_inner(self, x, y) -> FieldArray: + if self._less_than: + return self._gf(int(int(x) < int(y))) + else: + return self._gf(int(int(x) > int(y))) + + def _arithmetize_inner(self, strategy: str) -> Node: + p = self._gf.characteristic + + if self._less_than: + left = self._left + right = self._right + else: + left = self._right + right = self._left + + left = left.arithmetize(strategy) + right = right.arithmetize(strategy) + + left_is_small = SemiStrictComparison( + left, Constant(self._gf(p // 2)), less_than=True, gf=self._gf + ) + right_is_small = SemiStrictComparison( + right, Constant(self._gf(p // 2)), less_than=True, gf=self._gf + ) + + # Test whether left and right are in the same range + same_range = (left_is_small & right_is_small) + ( + Neg(left_is_small, self._gf) & Neg(right_is_small, self._gf) + ) + + # Performs left < right on the reduced inputs, note that if both are in the upper half the difference is still small enough for a semi-comparison + comparison = SemiStrictComparison(left, right, less_than=True, gf=self._gf) + result = same_range * comparison + + # Performs left < right when one if small and the other is large + right_is_larger = left_is_small & Neg(right_is_small, self._gf) + result += right_is_larger + + return result.arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + p = self._gf.characteristic + + if self._less_than: + left = self._left + right = self._right + else: + left = self._right + right = self._left + + left_front = left.arithmetize_depth_aware(cost_of_squaring) + right_front = right.arithmetize_depth_aware(cost_of_squaring) + + # TODO: This is just exhaustive. We can instead add a method decompose so that we do not have to copy this from arithmetize. + front = CostParetoFront(cost_of_squaring) + + for _, _, left_node in left_front: + for _, _, right_node in right_front: + left_is_small = SemiStrictComparison( + left_node, Constant(self._gf(p // 2)), less_than=True, gf=self._gf + ) + right_is_small = SemiStrictComparison( + right_node, Constant(self._gf(p // 2)), less_than=True, gf=self._gf + ) + + # Test whether left and right are in the same range + same_range = (left_is_small & right_is_small) + ( + Neg(left_is_small, self._gf) & Neg(right_is_small, self._gf) + ) + + # Performs left < right on the reduced inputs, note that if both are in the upper half the difference is still small enough for a semi-comparison + comparison = SemiStrictComparison( + left_node, right_node, less_than=True, gf=self._gf + ) + result = same_range * comparison + + # Performs left < right when one if small and the other is large + right_is_larger = left_is_small & Neg(right_is_small, self._gf) + result += right_is_larger + + front.add_front(result.arithmetize_depth_aware(cost_of_squaring)) + + return front + + +class SemiComparison(AbstractComparison): + """A node representing a comparison x <= y or x >= y that only works when x and y are at most p // 2 elements apart.""" + + @property + def _hash_name(self) -> str: + return "semi_comparison" + + @property + def _node_label(self) -> str: + return "~<=" if self._less_than else ">=~" + + def _operation_inner(self, x, y) -> FieldArray: + assert abs(int(x) - int(y)) <= self._gf.characteristic // 2 + + if self._less_than: + return self._gf(int(int(x) <= int(y))) + else: + return self._gf(int(int(x) >= int(y))) + + def _arithmetize_inner(self, strategy: str) -> Node: + return Neg( + SemiStrictComparison( + self._left.arithmetize(strategy), + self._right.arithmetize(strategy), + less_than=not self._less_than, + gf=self._gf, + ), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return Neg( + SemiStrictComparison( + self._left, self._right, less_than=not self._less_than, gf=self._gf + ), + self._gf, + ).arithmetize_depth_aware(cost_of_squaring) + + +class Comparison(AbstractComparison): + """A node representing a comparison x <= y or x >= y.""" + + @property + def _hash_name(self) -> str: + return "comparison" + + @property + def _node_label(self) -> str: + return "<=" if self._less_than else ">=" + + def _operation_inner(self, x, y) -> FieldArray: + if self._less_than: + return self._gf(int(int(x) <= int(y))) + else: + return self._gf(int(int(x) >= int(y))) + + def _arithmetize_inner(self, strategy: str) -> Node: + return Neg( + StrictComparison( + self._left.arithmetize(strategy), + self._right.arithmetize(strategy), + less_than=not self._less_than, + gf=self._gf, + ), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return Neg( + StrictComparison(self._left, self._right, less_than=not self._less_than, gf=self._gf), + self._gf, + ).arithmetize_depth_aware(cost_of_squaring) + + +class T2SemiLessThan(NonCommutativeBinaryNode): + """Implementation of [the algorithm from the T2 framework](https://petsymposium.org/popets/2023/popets-2023-0075.pdf) for performing x < y.""" + + @property + def _hash_name(self) -> str: + return "less_than_t2" + + @property + def _node_label(self) -> str: + return "< [t2]" + + def _operation_inner(self, x, y) -> FieldArray: + return self._gf(int(int(x) < int(y))) + + def _arithmetize_inner(self, strategy: str) -> Node: + out = Constant(self._gf(0)) + + p = self._gf.characteristic + for a in range((p + 1) // 2, p): + out += Constant(self._gf(1)) - (self._left - self._right - Constant(self._gf(a))) ** ( + p - 1 + ) + + return out.arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + raise NotImplementedError() + + +class IliashenkoZuccaSemiLessThan(NonCommutativeBinaryNode): + """Implementation of the [Illiashenko-Zucca algorithm](https://eprint.iacr.org/2021/315) for performing x < y.""" + + @property + def _hash_name(self) -> str: + return "less_than_t2" + + @property + def _node_label(self) -> str: + return "< [t2]" + + def _operation_inner(self, x, y) -> FieldArray: + return self._gf(int(int(x) < int(y))) + + def _arithmetize_inner(self, strategy: str) -> Node: + return IliashenkoZuccaInUpperHalf( + Subtraction( + self._left.arithmetize(strategy), self._right.arithmetize(strategy), self._gf + ), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + raise NotImplementedError() + + +def test_evaluate_semi_mod5_lt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiStrictComparison(a, b, less_than=True, gf=gf) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod5_lt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiStrictComparison(a, b, less_than=True, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_mod5_lt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = StrictComparison(a, b, less_than=True, gf=gf) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_mod5_lt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = StrictComparison(a, b, less_than=True, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_mod11_lt(): # noqa: D103 + gf = GF(11) + + a = Input("a", gf) + b = Input("b", gf) + node = StrictComparison(a, b, less_than=True, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(11): + for y in range(11): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_depth_aware_semi_mod11_lt(): # noqa: D103 + gf = GF(11) + + a = Input("a", gf) + b = Input("b", gf) + front = SemiStrictComparison(a, b, less_than=True, gf=gf).arithmetize_depth_aware(1.0) + + for _, _, node in front: + for x in range(6): + for y in range(6): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_depth_aware_mod11_lt(): # noqa: D103 + gf = GF(11) + + a = Input("a", gf) + b = Input("b", gf) + front = StrictComparison(a, b, less_than=True, gf=gf).arithmetize_depth_aware(1.0) + + for _, _, node in front: + for x in range(11): + for y in range(11): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod5_t2(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = T2SemiLessThan(a, b, gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod11_t2(): # noqa: D103 + gf = GF(11) + + a = Input("a", gf) + b = Input("b", gf) + node = T2SemiLessThan(a, b, gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(6): + for y in range(6): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_semi_mod5_gt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiStrictComparison(a, b, less_than=False, gf=gf) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x > y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod5_gt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiStrictComparison(a, b, less_than=False, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x > y)) + node.clear_cache(set()) + + +def test_evaluate_mod5_gt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = StrictComparison(a, b, less_than=False, gf=gf) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x > y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_mod5_gt(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = StrictComparison(a, b, less_than=False, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x > y)) + node.clear_cache(set()) + + +def test_evaluate_semi_mod5_ge(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiComparison(a, b, less_than=False, gf=gf) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x >= y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod5_ge(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiComparison(a, b, less_than=False, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x >= y)) + node.clear_cache(set()) + + +def test_evaluate_mod5_ge(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Comparison(a, b, less_than=False, gf=gf) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x >= y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_mod5_ge(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Comparison(a, b, less_than=False, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x >= y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_depth_aware_mod5_ge(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Comparison(a, b, less_than=False, gf=gf) + front = node.arithmetize_depth_aware(0.75) + + for _, _, n in front: + for x in range(5): + for y in range(5): + assert n.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x >= y)) + n.clear_cache(set()) + + +def test_evaluate_semi_mod5_le(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiComparison(a, b, less_than=True, gf=gf) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x <= y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod5_le(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiComparison(a, b, less_than=True, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(3): + for y in range(3): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x <= y)) + node.clear_cache(set()) + + +def test_evaluate_mod5_le(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Comparison(a, b, less_than=True, gf=gf) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x <= y)) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_mod5_le(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Comparison(a, b, less_than=True, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(5): + for y in range(5): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x <= y)) + node.clear_cache(set()) + + +def test_evaluate_semi_arithmetized_mod101_lt(): # noqa: D103 + gf = GF(101) + + a = Input("a", gf) + b = Input("b", gf) + node = SemiStrictComparison(a, b, less_than=True, gf=gf).arithmetize("best-effort") + node.clear_cache(set()) + + for x in range(51): + for y in range(51): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_semi_depth_aware_arithmetized_mod61_lt(): # noqa: D103 + gf = GF(61) + + a = Input("a", gf) + b = Input("b", gf) + front = SemiStrictComparison(a, b, less_than=True, gf=gf).arithmetize_depth_aware(cost_of_squaring=1.0) + + for _, _, node in front: + node.clear_cache(set()) + + for x in range(31): + for y in range(31): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_evaluate_semi_depth_aware_arithmetized_mod61_lt_05sq(): # noqa: D103 + gf = GF(61) + + a = Input("a", gf) + b = Input("b", gf) + front = SemiStrictComparison(a, b, less_than=True, gf=gf).arithmetize_depth_aware(cost_of_squaring=0.5) + + for _, _, node in front: + node.clear_cache(set()) + + for x in range(31): + for y in range(31): + assert node.evaluate({"a": gf(x), "b": gf(y)}) == gf(int(x < y)) + node.clear_cache(set()) + + +def test_lessthan_mod101(): # noqa: D103 + gf = GF(101) + + x = Input("x", gf) + circuit = Circuit([x < 30]) + + for _, _, arithmetization in circuit.arithmetize_depth_aware(): + assert arithmetization.evaluate({ + "x": gf(90), + })[0] == 0 diff --git a/oraqle/compiler/comparison/equality.py b/oraqle/compiler/comparison/equality.py new file mode 100644 index 0000000..cf6f3a9 --- /dev/null +++ b/oraqle/compiler/comparison/equality.py @@ -0,0 +1,106 @@ +"""This module contains classes for representing equality checks.""" +from galois import GF, FieldArray + +from oraqle.compiler.arithmetic.exponentiation import Power +from oraqle.compiler.arithmetic.subtraction import Subtraction +from oraqle.compiler.boolean.bool_neg import Neg +from oraqle.compiler.nodes.abstract import CostParetoFront, Node +from oraqle.compiler.nodes.binary_arithmetic import CommutativeBinaryNode +from oraqle.compiler.nodes.leafs import Input +from oraqle.compiler.nodes.univariate import UnivariateNode + + +class IsNonZero(UnivariateNode): + """This node represents a zero check: x == 0.""" + + @property + def _node_shape(self) -> str: + return "box" + + @property + def _hash_name(self) -> str: + return "is_nonzero" + + @property + def _node_label(self) -> str: + return "!= 0" + + def _operation_inner(self, input: FieldArray) -> FieldArray: + return input != 0 + + def _arithmetize_inner(self, strategy: str) -> Node: + return Power(self._node, self._gf.order - 1, self._gf).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return Power(self._node, self._gf.order - 1, self._gf).arithmetize_depth_aware( + cost_of_squaring + ) + + +class Equals(CommutativeBinaryNode): + """This node represents an equality operation: x == y.""" + + @property + def _hash_name(self) -> str: + return "equals" + + @property + def _node_label(self) -> str: + return "==" + + def _operation_inner(self, x, y) -> FieldArray: + return self._gf(int(x == y)) + + def _arithmetize_inner(self, strategy: str) -> Node: + return Neg( + IsNonZero(Subtraction(self._left, self._right, self._gf), self._gf), + self._gf, + ).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return Neg( + IsNonZero(Subtraction(self._left, self._right, self._gf), self._gf), + self._gf, + ).arithmetize_depth_aware(cost_of_squaring) + + +def test_evaluate_mod5(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Equals(a, b, gf) + + assert node.evaluate({"a": gf(3), "b": gf(2)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(4), "b": gf(4)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(2)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(0)}) == gf(1) + + +def test_evaluate_arithmetized_mod5(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + node = Equals(a, b, gf).arithmetize("best-effort") + node.clear_cache(set()) + + assert node.evaluate({"a": gf(3), "b": gf(2)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(4), "b": gf(4)}) == gf(1) + node.clear_cache(set()) + assert node.evaluate({"a": gf(1), "b": gf(2)}) == gf(0) + node.clear_cache(set()) + assert node.evaluate({"a": gf(0), "b": gf(0)}) == gf(1) + + +def test_equality_equivalence_commutative(): # noqa: D103 + gf = GF(5) + + a = Input("a", gf) + b = Input("b", gf) + + assert (a == b).is_equivalent(b == a) diff --git a/oraqle/compiler/comparison/in_upper_half.py b/oraqle/compiler/comparison/in_upper_half.py new file mode 100644 index 0000000..5cdc1f6 --- /dev/null +++ b/oraqle/compiler/comparison/in_upper_half.py @@ -0,0 +1,275 @@ +"""This module contains classes for checking if an element is in the upper half of the finite field.""" +import math + +from galois import GF, FieldArray + +from oraqle.add_chains.addition_chains_front import gen_pareto_front +from oraqle.add_chains.addition_chains_heuristic import add_chain_guaranteed +from oraqle.add_chains.solving import extract_indices +from oraqle.compiler.nodes.abstract import CostParetoFront, Node +from oraqle.compiler.nodes.binary_arithmetic import Addition, Multiplication +from oraqle.compiler.nodes.leafs import Input +from oraqle.compiler.nodes.unary_arithmetic import ConstantMultiplication +from oraqle.compiler.nodes.univariate import UnivariateNode +from oraqle.compiler.polynomials.univariate import UnivariatePoly, _eval_poly + + +class InUpperHalf(UnivariateNode): + """Returns 1 when the input is contained in the upper half of the field, which are considered the negative numbers. + + Specifically, it returns 0 in the range [0, (p - 1) / 2] and 1 in the range ((p - 1) / 2, p - 1]. + """ + + @property + def _node_shape(self) -> str: + return "box" + + @property + def _hash_name(self) -> str: + return "in_upper_half" + + @property + def _node_label(self) -> str: + return "> (p-1)/2" + + def _operation_inner(self, input: FieldArray) -> FieldArray: + p = self._gf.characteristic + if 0 <= int(input) <= p // 2: + return self._gf(0) + + return self._gf(1) + + def _arithmetize_inner(self, strategy: str) -> Node: + coefficients = [] + + # From: Faster homomorphic comparison operations for BGV and BFV, Ilia Iliashenko & Vincent Zucca, 2021 + p = self._gf.characteristic + for i in range(p - 1): + if i % 2 == 0: + # Ignore every even power, we take care of this by squaring the input node. + continue + + coefficient = self._gf(0) + for a in range(1, p // 2 + 1): + coefficient += self._gf(a) ** (p - 1 - i) + coefficients.append(coefficient) + + # We do not add the final coefficient, which will be computed later, so we do not do coefficients.append(gf((p + 1) // 2)) + + input_node = self._node.arithmetize(strategy).to_arithmetic() + input_node_squared = input_node * input_node + arithmetization, precomputed_powers = UnivariatePoly( + input_node_squared, coefficients, self._gf + ).arithmetize_custom(strategy) + + # Since we skip the first coefficient, we manually multiply the output by the input node. + result = Multiplication(input_node, arithmetization, self._gf) + + # Compute the final coefficient using an exponentiation + precomputed_values = tuple( + ( + (2 * exp) % (p - 1), + power_node.multiplicative_depth() - input_node.multiplicative_depth(), + ) + for exp, power_node in precomputed_powers.items() + ) + + addition_chain = add_chain_guaranteed(p - 1, p - 1, squaring_cost=1.0, precomputed_values=precomputed_values) + + nodes = [input_node] + nodes.extend(power_node for _, power_node in precomputed_powers.items()) + + for i, j in addition_chain: + nodes.append(Multiplication(nodes[i], nodes[j], self._gf)) + + final_term = ConstantMultiplication(nodes[-1], self._gf((p + 1) // 2)) + + return (Addition(result, final_term, self._gf)).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + # TODO: Handle p = 2 and p = 3 separately + + # TODO: Reduce code duplication + final_front = CostParetoFront(cost_of_squaring) + + for node_depth, _, node in self._node.arithmetize_depth_aware(cost_of_squaring): + coefficients = [] + + # From: Faster homomorphic comparison operations for BGV and BFV, Ilia Iliashenko & Vincent Zucca, 2021 + p = self._gf.characteristic + for i in range(p - 1): + if i % 2 == 0: + # Ignore every even power, we take care of this by squaring the input node. + continue + + coefficient = self._gf(0) + for a in range(1, p // 2 + 1): + coefficient += self._gf(a) ** (p - 1 - i) + coefficients.append(coefficient) + + # We do not add the final coefficient, which will be computed later, so we do not do coefficients.append(gf((p + 1) // 2)) + + input_node_squared = Multiplication(node, node, self._gf) + arithmetizations, precomputed_powers = UnivariatePoly( + input_node_squared, coefficients, self._gf + ).arithmetize_depth_aware_custom(cost_of_squaring) + + assert not arithmetizations.is_empty() + + for depth, _, poly_arith in arithmetizations: + # Since we skip the first coefficient, we manually multiply the output by the input node. + result = Multiplication(node, poly_arith, self._gf) + + # Compute the final coefficient using an exponentiation + precomputed_values = tuple( + ((2 * exp) % (p - 1), power_node.multiplicative_depth() - node_depth) + for exp, power_node in precomputed_powers[depth].items() + ) + # TODO: This is copied from Power, but in the future we can probably remove this if we have augmented circuits + if p <= 200: + front = gen_pareto_front( + p - 1, + self._gf.characteristic - 1, + cost_of_squaring, + precomputed_values=precomputed_values, + ) + else: + front = gen_pareto_front( + p - 1, None, cost_of_squaring, precomputed_values=precomputed_values + ) + + final_power_front = CostParetoFront(cost_of_squaring) + + for depth2, chain in front: + c = extract_indices( + chain, + precomputed_values=list(k for k, _ in precomputed_values), + modulus=p - 1, + ) + + nodes = [node] + nodes.extend(power_node for _, power_node in precomputed_powers[depth].items()) + + for i, j in c: + nodes.append(Multiplication(nodes[i], nodes[j], self._gf)) + + final_power_front.add(nodes[-1], depth=node_depth + depth2) + + for _, _, final_power in final_power_front: + final_term = ConstantMultiplication(final_power, self._gf((p + 1) // 2)) + final_front.add(Addition(result, final_term, self._gf)) + + assert not final_front.is_empty() + return final_front + + +class IliashenkoZuccaInUpperHalf(UnivariateNode): + """Returns 1 when the input is contained in the upper half of the field, which are considered the negative numbers. + + Specifically, it returns 0 in the range [0, (p - 1) / 2] and 1 in the range ((p - 1) / 2, p - 1]. + """ + + @property + def _node_shape(self) -> str: + return "box" + + @property + def _hash_name(self) -> str: + return "in_upper_half_iz21" + + @property + def _node_label(self) -> str: + return "> (p-1)/2 [IZ21]" + + def _operation_inner(self, input: FieldArray) -> FieldArray: + p = self._gf.characteristic + if 0 <= int(input) <= p // 2: + return self._gf(0) + + return self._gf(1) + + def _arithmetize_inner(self, strategy: str) -> Node: + coefficients = [] + + # TODO: This is copied from above + # From: Faster homomorphic comparison operations for BGV and BFV, Ilia Iliashenko & Vincent Zucca, 2021 + p = self._gf.characteristic + for i in range(p - 1): + if i % 2 == 0: + # Ignore every even power, we take care of this by squaring the input node. + continue + + coefficient = self._gf(0) + for a in range(1, p // 2 + 1): + coefficient += self._gf(a) ** (p - 1 - i) + coefficients.append(coefficient) + + # We do not add the final coefficient, which will be computed later, so we do not do coefficients.append(gf((p + 1) // 2)) + + input_node = self._node.arithmetize(strategy).to_arithmetic() + input_node_squared = Multiplication(input_node, input_node, self._gf) + + # We decide ahead of time which k to use + k = round(math.sqrt((p - 3) / 2)) + arithmetization, precomputed_powers = _eval_poly( + input_node_squared, coefficients, k, self._gf, squaring_cost=1.0 + ) + + # Since we skip the first coefficient, we manually multiply the output by the input node. + result = Multiplication(input_node, arithmetization, self._gf) + + # Compute the final coefficient using an exponentiation + precomputed_values = tuple( + ( + (2 * exp) % (p - 1), + power_node.multiplicative_depth() - input_node.multiplicative_depth(), + ) + for exp, power_node in precomputed_powers.items() + ) + + addition_chain = add_chain_guaranteed(p - 1, p - 1, squaring_cost=1.0, precomputed_values=precomputed_values) + + nodes = [input_node] + nodes.extend(power_node for _, power_node in precomputed_powers.items()) + + for i, j in addition_chain: + nodes.append(Multiplication(nodes[i], nodes[j], self._gf)) + final_monomial = nodes[-1] + + final_term = ConstantMultiplication(final_monomial, self._gf((p + 1) // 2)) + + return (Addition(result, final_term, self._gf)).arithmetize(strategy) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + raise NotImplementedError() + # TODO: Handle p = 2 and p = 3 separately + + +# TODO: Make a univariate node class with an easy way to test if evaluation corresponds to evaluating the arithmetic +def test_evaluate_mod7(): # noqa: D103 + gf = GF(7) + + x = Input("x", gf) + node = InUpperHalf(x, gf) + + for i in range(3): + assert node.evaluate({"x": gf(i)}) == gf(0) + node.clear_cache(set()) + for i in range(4, 7): + assert node.evaluate({"x": gf(i)}) == gf(1) + node.clear_cache(set()) + + +def test_evaluate_arithmetized_mod7(): # noqa: D103 + gf = GF(7) + + x = Input("x", gf) + node = InUpperHalf(x, gf).arithmetize("best-effort") + node.clear_cache(set()) + + for i in range(3): + assert node.evaluate({"x": gf(i)}) == gf(0) + node.clear_cache(set()) + for i in range(4, 7): + assert node.evaluate({"x": gf(i)}) == gf(1) + node.clear_cache(set()) diff --git a/oraqle/compiler/control_flow/__init__.py b/oraqle/compiler/control_flow/__init__.py new file mode 100644 index 0000000..536cf1d --- /dev/null +++ b/oraqle/compiler/control_flow/__init__.py @@ -0,0 +1 @@ +"""This package contains control flow functions.""" diff --git a/oraqle/compiler/control_flow/conditional.py b/oraqle/compiler/control_flow/conditional.py new file mode 100644 index 0000000..2a8ba03 --- /dev/null +++ b/oraqle/compiler/control_flow/conditional.py @@ -0,0 +1,110 @@ +"""This module contains tools for evaluating conditional statements.""" +from typing import List, Type + +from galois import GF, FieldArray + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.abstract import CostParetoFront, Node +from oraqle.compiler.nodes.fixed import FixedNode +from oraqle.compiler.nodes.leafs import Constant, Input + + +class IfElse(FixedNode): + """A node representing an if-else clause.""" + + @property + def _node_label(self): + return "If" + + @property + def _hash_name(self): + return "if_else" + + def __init__(self, condition: Node, positive: Node, negative: Node, gf: Type[FieldArray]): + """Initialize an if-else node: If condition evaluates to true, then it outputs positive, otherwise it outputs negative.""" + self._condition = condition + self._positive = positive + self._negative = negative + super().__init__(gf) + + def __hash__(self) -> int: + return hash((self._hash_name, self._condition, self._positive, self._negative)) + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + return ( + self._condition.is_equivalent(other._condition) + and self._positive.is_equivalent(other._positive) + and self._negative.is_equivalent(other._negative) + ) + + def operands(self) -> List[Node]: # noqa: D102 + return [self._condition, self._positive, self._negative] + + def set_operands(self, operands: List[Node]): # noqa: D102 + self._condition = operands[0] + self._positive = operands[1] + self._negative = operands[2] + + def operation(self, operands: List[FieldArray]) -> FieldArray: # noqa: D102 + assert operands[0] == 0 or operands[0] == 1 + return operands[1] if operands[0] == 1 else operands[2] + + def _arithmetize_inner(self, strategy: str) -> Node: + return (self._condition * (self._positive - self._negative) + self._negative).arithmetize( + strategy + ) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return ( + self._condition * (self._positive - self._negative) + self._negative + ).arithmetize_depth_aware(cost_of_squaring) + + +def if_else(condition: Node, positive: Node, negative: Node) -> IfElse: + """Sugar expression for creating an if-else clause. + + Returns: + An `IfElse` node that equals `positive` if `condition` is true, and `negative` otherwise. + """ + assert condition._gf == positive._gf + assert condition._gf == negative._gf + return IfElse(condition, positive, negative, condition._gf) + + +def test_if_else(): # noqa: D103 + gf = GF(11) + + a = Input("a", gf) + b = Input("b", gf) + + output = if_else(a == b, Constant(gf(3)), Constant(gf(5))) + + circuit = Circuit([output]) + + for val_a in range(11): + for val_b in range(11): + expected = gf(3) if val_a == val_b else gf(5) + + values = {"a": gf(val_a), "b": gf(val_b)} + assert circuit.evaluate(values) == expected + + +def test_if_else_arithmetized(): # noqa: D103 + gf = GF(11) + + a = Input("a", gf) + b = Input("b", gf) + + output = if_else(a == b, Constant(gf(3)), Constant(gf(5))) + + arithmetic_circuit = Circuit([output]).arithmetize() + + for val_a in range(11): + for val_b in range(11): + expected = gf(3) if val_a == val_b else gf(5) + + values = {"a": gf(val_a), "b": gf(val_b)} + assert arithmetic_circuit.evaluate(values) == expected diff --git a/oraqle/compiler/func2poly.py b/oraqle/compiler/func2poly.py new file mode 100644 index 0000000..d78a2dc --- /dev/null +++ b/oraqle/compiler/func2poly.py @@ -0,0 +1,44 @@ +"""Tools for interpolating polynomials from arbitrary functions.""" +import itertools +from typing import Callable, List + +from sympy import Poly, symbols + + +def principal_character(x, prime_modulus): + """Computes the principal character. This expression always returns 1 when x = 0 and 0 otherwise. Only works for prime moduli. + + Returns: + The principal character x**(p-1). + """ + return x ** (prime_modulus - 1) + + +def interpolate_polynomial( + function: Callable[..., int], prime_modulus: int, input_names: List[str] +) -> Poly: + """Interpolates a polynomial for the given function. This is currently only implemented for prime moduli. This function interpolates the polynomial on all possible inputs. + + Returns: + A sympy `Poly` object representing the unique polynomial that evaluates to the same outputs for all inputs as `function`. + """ + variables = symbols(input_names) + poly = 0 + + for inputs in itertools.product(range(prime_modulus), repeat=len(input_names)): + output = function(*inputs) + assert 0 <= output < prime_modulus + + product = output + for input, variable in zip(inputs, variables): + product *= Poly( + 1 - principal_character(variable - input, prime_modulus), + variable, + modulus=prime_modulus, + ) + product = Poly(product, variables, modulus=prime_modulus) + + poly += product + poly = Poly(poly, variables, modulus=prime_modulus) + + return Poly(poly, variables, modulus=prime_modulus) diff --git a/oraqle/compiler/graphviz.py b/oraqle/compiler/graphviz.py new file mode 100644 index 0000000..d56cb80 --- /dev/null +++ b/oraqle/compiler/graphviz.py @@ -0,0 +1,56 @@ +"""This module contains classes and functions for visualizing circuits using graphviz.""" +from typing import Dict, List, Tuple + +expensive_style = {"shape": "diamond"} + + +class DotFile: + """A `DotFile` is a graph description format that can be rendered to e.g. PDF using graphviz.""" + + def __init__(self): + """Initialize an empty DotFile.""" + self._nodes: List[Dict[str, str]] = [] + self._links: List[Tuple[int, int, Dict[str, str]]] = [] + + def add_node(self, **kwargs) -> int: + """Adds a node to the file. The keyword arguments are directly put into the DOT file. + + For example, one can specify a label, a color, a style, etc... + + Returns: + The identifier of this node in this `DotFile`. + """ + node_id = len(self._nodes) + self._nodes.append(kwargs) + + return node_id + + def add_link(self, from_id: int, to_id: int, **kwargs): + """Adds an unformatted link between the nodes with `from_id` and `to_id`. The keyword arguments are directly put into the DOT file.""" + self._links.append((from_id, to_id, kwargs)) + + def to_file(self, filename: str): + """Writes the DOT file to the given filename as a directed graph called 'G'.""" + with open(filename, mode="w", encoding="utf-8") as file: + file.write("digraph G {\n") + file.write('forcelabels="true";\n') + file.write("graph [nodesep=0.25,ranksep=0.6];") # nodesep, ranksep + + # Write all the nodes + for node_id, attributes in enumerate(self._nodes): + transformed_attributes = ",".join( + [f'{key}="{value}"' for key, value in attributes.items()] + ) + file.write(f"n{node_id} [{transformed_attributes}];\n") + + # Write all the links + for from_id, to_id, attributes in self._links: + if len(attributes) == 0: + file.write(f"n{from_id}->n{to_id};\n") + else: + text = f"n{from_id}->n{to_id} [" + text += ",".join((f"{key}={value}" for key, value in attributes.items())) + text += "];\n" + file.write(text) + + file.write("}\n") diff --git a/oraqle/compiler/instructions.py b/oraqle/compiler/instructions.py new file mode 100644 index 0000000..10e1c8e --- /dev/null +++ b/oraqle/compiler/instructions.py @@ -0,0 +1,241 @@ +"""This module contains the classes that represent instructions and programs for evaluating arithmetic circuits.""" +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Type + +from galois import GF, FieldArray + + +class ArithmeticInstruction(ABC): + """An abstract arithmetic instruction that computes an operation in an arithmetic circuit using a stack.""" + + def __init__(self, stack_index: int) -> None: + """Initialize an instruction that writes it output to the stack at `stack_index`.""" + self._stack_index = stack_index + + @abstractmethod + def evaluate( + self, stack: List[Optional[FieldArray]], inputs: Dict[str, FieldArray] + ) -> Optional[FieldArray]: + """Executes the instruction on plaintext inputs without using encryption, keeping track of the plaintext values in the stack.""" + + @abstractmethod + def generate_code(self, stack_initialized: List[bool], decrypt_outputs: bool) -> str: + """Generates code for this instruction, keeping track of which places of the stack are already initialized.""" + + +class AdditionInstruction(ArithmeticInstruction): + """Reads two elements from the stack, adds them, and writes the result to the stack.""" + + def __init__(self, stack_index: int, left_stack_index: int, right_stack_index: int) -> None: + """Initialize an instruction that adds the elements at `left_stack_index` and `right_stack_index`, placing the result at `stack_index`.""" + self._left_stack_index = left_stack_index + self._right_stack_index = right_stack_index + super().__init__(stack_index) + + def evaluate(self, stack: List[Optional[FieldArray]], _inputs: Dict[str, FieldArray]) -> None: # noqa: D102 + left = stack[self._left_stack_index] + right = stack[self._right_stack_index] + assert left is not None + assert right is not None + stack[self._stack_index] = left + right + + def generate_code(self, stack_initialized: List[bool], _decrypt_outputs: bool) -> str: # noqa: D102 + if self._left_stack_index == self._stack_index: + return f"stack_{self._stack_index} += stack_{self._right_stack_index};\n" + if self._right_stack_index == self._stack_index: + return f"stack_{self._stack_index} += stack_{self._left_stack_index};\n" + + code = "" + if not stack_initialized[self._stack_index]: + code += "ctxt_t " + code += f"stack_{self._stack_index} = stack_{self._left_stack_index};\nstack_{self._stack_index} += stack_{self._right_stack_index};\n" + stack_initialized[self._stack_index] = True + return code + + +class MultiplicationInstruction(ArithmeticInstruction): + """Reads two elements from the stack, multiplies them, and writes the result to the stack.""" + + def __init__(self, stack_index: int, left_stack_index: int, right_stack_index: int) -> None: + """Initialize an instruction that multiplies the elements at `left_stack_index` and `right_stack_index`, placing the result at `stack_index`.""" + self._left_stack_index = left_stack_index + self._right_stack_index = right_stack_index + super().__init__(stack_index) + + def evaluate(self, stack: List[Optional[FieldArray]], _inputs: Dict[str, FieldArray]) -> None: # noqa: D102 + left = stack[self._left_stack_index] + right = stack[self._right_stack_index] + assert left is not None + assert right is not None + stack[self._stack_index] = left * right + + def generate_code(self, stack_initialized: List[bool], _decrypt_outputs: bool) -> str: # noqa: D102 + if self._left_stack_index == self._stack_index: + return f"stack_{self._stack_index} *= stack_{self._right_stack_index};\n" + if self._right_stack_index == self._stack_index: + return f"stack_{self._stack_index} *= stack_{self._left_stack_index};\n" + + code = "" + if not stack_initialized[self._stack_index]: + code += "ctxt_t " + code += f"stack_{self._stack_index} = stack_{self._left_stack_index};\nstack_{self._stack_index} *= stack_{self._right_stack_index};\n" + stack_initialized[self._stack_index] = True + return code + + +class ConstantAdditionInstruction(ArithmeticInstruction): + """Reads an element from the stack, adds a constant to it it, and writes the result to the stack.""" + + def __init__(self, stack_index: int, input_stack_index: int, constant: FieldArray) -> None: + """Initialize an instruction that adds `constant` to the element at `input_stack_index`, placing the result at `stack_index`.""" + self._input_stack_index = input_stack_index + self._constant = constant + super().__init__(stack_index) + + def evaluate(self, stack: List[Optional[FieldArray]], _inputs: Dict[str, FieldArray]) -> None: # noqa: D102 + operand = stack[self._input_stack_index] + assert operand is not None + stack[self._stack_index] = operand + self._constant + + def generate_code(self, stack_initialized: List[bool], _decrypt_outputs: bool) -> str: # noqa: D102 + if self._stack_index == self._input_stack_index: + return f"stack_{self._input_stack_index} += {self._constant}l;\n" + + code = "" + if not stack_initialized[self._stack_index]: + code += "ctxt_t " + code += f"stack_{self._stack_index} = stack_{self._input_stack_index};\nstack_{self._stack_index} += {self._constant}l;\n" + stack_initialized[self._stack_index] = True + return code + + +class ConstantMultiplicationInstruction(ArithmeticInstruction): + """Reads an element from the stack, multiplies it with a constant, and writes the result to the stack.""" + + def __init__(self, stack_index: int, input_stack_index: int, constant: FieldArray) -> None: + """Initialize an instruction that multiplies the element at `input_stack_index` with `constant`, placing the result at `stack_index`.""" + self._input_stack_index = input_stack_index + self._constant = constant + super().__init__(stack_index) + + def evaluate(self, stack: List[Optional[FieldArray]], _inputs: Dict[str, FieldArray]) -> None: # noqa: D102 + operand = stack[self._input_stack_index] + assert operand is not None + stack[self._stack_index] = operand * self._constant + + def generate_code(self, stack_initialized: List[bool], _decrypt_outputs: bool) -> str: # noqa: D102 + if self._stack_index == self._input_stack_index: + return f"stack_{self._input_stack_index} *= {self._constant}l;\n" + + code = "" + if not stack_initialized[self._stack_index]: + code += "ctxt_t " + code += f"stack_{self._stack_index} = stack_{self._input_stack_index};\nstack_{self._stack_index} *= {self._constant}l;\n" + stack_initialized[self._stack_index] = True + return code + + +class InputInstruction(ArithmeticInstruction): + """Writes an input to the stack.""" + + def __init__(self, stack_index: int, name: str) -> None: + """Initialize an `InputInstruction` that places the input with the given `name` in the stack at index `stack_index`.""" + self._name = name + super().__init__(stack_index) + + def evaluate(self, stack: List[Optional[FieldArray]], inputs: Dict[str, FieldArray]) -> None: # noqa: D102 + stack[self._stack_index] = inputs[self._name] + + def generate_code(self, stack_initialized: List[bool], _decrypt_outputs: bool) -> str: # noqa: D102 + code = "" + if not stack_initialized[self._stack_index]: + code += "ctxt_t " + code += f"stack_{self._stack_index} = ciph_{self._name};\n" + stack_initialized[self._stack_index] = True + return code + + +class OutputInstruction(ArithmeticInstruction): + """Outputs an element from the stack.""" + + def evaluate(self, stack: List[FieldArray], _inputs: Dict[str, FieldArray]) -> FieldArray: # noqa: D102 + return stack[self._stack_index] + + def generate_code(self, stack_initialized: List[bool], decrypt_outputs: bool) -> str: # noqa: D102 + if decrypt_outputs: + return f"ptxt_t decrypted(context);\nsecret_key.Decrypt(decrypted, stack_{self._stack_index});\nstd::cout << decrypted << std::endl;\n" + else: + return f'std::cout << "Output correctness: " << stack_{self._stack_index}.isCorrect() << std::endl;\n' + + +class ArithmeticProgram: + """An ArithmeticProgram represents an ordered set of arithmetic operations that compute an arithmetic circuit. + + The easiest way to obtain an `ArithmeticProgram` of an `ArithmeticCircuit` is to call `ArithmeticCircuit.generate_program()`. + """ + + def __init__( + self, instructions: List[ArithmeticInstruction], stack_size: int, gf: Type[FieldArray] + ) -> None: + """Initialize an `ArithmeticProgram` from a list of `instructions`. + + The user must specify an upper bound on the `stack_size` required. + """ + self._instructions = instructions + self._stack_size = stack_size + self._gf = gf + + def execute(self, inputs: Dict[str, FieldArray]) -> FieldArray: + """Executes the arithmetic program on plaintext inputs without using encryption. + + Raises: + Exception: If there were no outputs in this program. + + Returns: + The first output in this program. + """ + # FIXME: Currently only supports a single output + for input in inputs.values(): + assert isinstance(input, self._gf) + + stack: List[Optional[FieldArray]] = [None for _ in range(self._stack_size)] + + for instruction in self._instructions: + if (output := instruction.evaluate(stack, inputs)) is not None: + return output + + raise Exception("The program did not output anything") + + def generate_code(self, decrypt_outputs: bool) -> str: + """Generates HElib code for this program. + + If `decrypt_outputs` is true, then the generated code will decrypt the outputs at the end of the circuit. + + Returns: + The generated code as a string. + """ + code = "" + stack_initialized = [False] * self._stack_size + + for instruction in self._instructions: + code += instruction.generate_code(stack_initialized, decrypt_outputs) + + return code + + +def test_instructions_small_comparison(): # noqa: D103 + from oraqle.compiler.circuit import Circuit + from oraqle.compiler.nodes.leafs import Input + + gf = GF(7) + + x = Input("x", gf) + y = Input("y", gf) + + arithmetic_circuit = Circuit([x < y]).arithmetize() + program = arithmetic_circuit.generate_program() + + for x in range(7): + for y in range(7): + inputs = {"x": gf(x), "y": gf(y)} + assert arithmetic_circuit.evaluate(inputs) == program.execute(inputs) diff --git a/oraqle/compiler/nodes/__init__.py b/oraqle/compiler/nodes/__init__.py new file mode 100644 index 0000000..2d7d435 --- /dev/null +++ b/oraqle/compiler/nodes/__init__.py @@ -0,0 +1,6 @@ +"""The nodes package contains a collection of fundamental abstract and concrete nodes.""" +from oraqle.compiler.nodes.abstract import Node +from oraqle.compiler.nodes.binary_arithmetic import Addition, Multiplication +from oraqle.compiler.nodes.leafs import Constant, Input + +__all__ = ['Addition', 'Constant', 'Input', 'Multiplication', 'Node'] diff --git a/oraqle/compiler/nodes/abstract.py b/oraqle/compiler/nodes/abstract.py new file mode 100644 index 0000000..4eef417 --- /dev/null +++ b/oraqle/compiler/nodes/abstract.py @@ -0,0 +1,783 @@ +"""Module containing the most fundamental classes in the compiler.""" +from abc import ABC, abstractmethod +from collections import Counter +from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Type, Union + +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.instructions import ArithmeticInstruction + + +def select_stack_index(stack_occupied: List[bool]) -> int: + """Selects a free index in the stack and occupies it. + + Returns: + The first free index in `stack_occupied`. + """ + for index, occupied in enumerate(stack_occupied): + if not occupied: + stack_occupied[index] = True + return index + + index = len(stack_occupied) + stack_occupied.append(True) + return index + + +# TODO: It would be great if we can move out this ParetoFront class, but it's hard to do without circular imports +class ParetoFront(ABC): + """Abstract base class for ParetoFronts. + + One objective is to minimize the multiplicative depth, while the other objective is minimizing some value, such as the multiplicative size or cost. + """ + + def __init__(self) -> None: + """Initialize an empty ParetoFront.""" + self._nodes_by_depth: Dict[int, Tuple[Union[int, float], ArithmeticNode]] = {} + self._highest_depth: int = -1 + + @abstractmethod + def _get_value(self, node: "ArithmeticNode") -> Union[int, float]: + pass + + @abstractmethod + def _default_value(self) -> Union[int, float]: + pass + + @classmethod + def from_node( + cls, + node: "ArithmeticNode", + depth: Optional[int] = None, + value: Optional[Union[int, float]] = None, + ) -> "ParetoFront": + """Initialize a `ParetoFront` with one node in it. + + Returns: + New `ParetoFront`. + """ + self = cls() + self.add(node, depth, value) + return self + + @classmethod + def from_leaf(cls, leaf) -> "ParetoFront": + """Initialize a `ParetoFront` with one leaf node in it. + + Returns: + New `ParetoFront`. + """ + self = cls() + self.add_leaf(leaf) + return self + + def add( + self, + node: "ArithmeticNode", + depth: Optional[int] = None, + value: Optional[Union[int, float]] = None, + ) -> bool: + """Adds the given `Node` to the `ParetoFront` by computing its multiplicative depth and value. + + Alternatively, the user can supply an unchecked `depth` and `value` so that these values do not have to be (re)computed. + + Returns: + `True` if and only if the node was inserted into the ParetoFront (so it was in some way better than the current `Nodes`). + """ + if depth is None: + depth = node.multiplicative_depth() + + if value is None: + value = self._get_value(node) + + return self._add(depth, value, node) + + def _add(self, depth: int, value: Union[int, float], node: "ArithmeticNode") -> bool: + """Returns True if and only if the node was inserted into the ParetoFront.""" + for d in range(depth + 1): + if d in self._nodes_by_depth and self._nodes_by_depth[d][0] <= value: + return False + + self._nodes_by_depth[depth] = (value, node) + self._highest_depth = max(depth, self._highest_depth) + + for d in range(depth + 1, self._highest_depth + 1): + if d in self._nodes_by_depth and self._nodes_by_depth[d][0] >= value: + del self._nodes_by_depth[d] + + return True + + def add_leaf(self, leaf): + """Add a leaf node to this `ParetoFront`.""" + self._add(0, 0, leaf) # type: ignore + + def add_front(self, front: "ParetoFront"): + """Add all elements from `front` to `self`.""" + # TODO: This can be optimized + for d, s, n in front: + self.add(n, d, s) + + def __iter__(self) -> Iterator[Tuple[int, Union[int, float], "ArithmeticNode"]]: + for depth in range(self._highest_depth + 1): + if depth in self._nodes_by_depth: + yield depth, self._nodes_by_depth[depth][0], self._nodes_by_depth[depth][1] + + def get_smallest_at_depth( + self, max_depth: int + ) -> Optional[Tuple[int, Union[int, float], "ArithmeticNode"]]: + """Returns the circuit with the smallest value that has at most depth `max_depth`.""" + for depth in reversed(range(max_depth + 1)): + if depth in self._nodes_by_depth: + return depth, self._nodes_by_depth[depth][0], self._nodes_by_depth[depth][1] + + def is_empty(self) -> bool: + """Returns whether the front is empty.""" + return len(self._nodes_by_depth) == 0 + + def get_lowest_value(self) -> Optional["ArithmeticNode"]: + """Returns the value (size or cost) of the Node with the highest depth, and therefore the lowest value.""" + if self._highest_depth == -1: + return None + + return self._nodes_by_depth[self._highest_depth][1] + + +def iterate_increasing_depth(front1: ParetoFront, front2: ParetoFront) -> Iterator[ + Tuple[ + Tuple[int, Union[int, float], "ArithmeticNode"], + Tuple[int, Union[int, float], "ArithmeticNode"], + ] +]: + """Iterates over two ParetoFronts, returning pairs of ArithmeticNodes such that the multiplicative depth grows monotonically. + + Yields: + Pairs of tuples, containing the multiplicative depth, the multiplicative size/cost, and the arithmetization, in that order. + """ + highest_depth = max(front1._highest_depth, front2._highest_depth) + last_depth: Optional[int] = None + + # TODO: This is quite inefficient because we constantly loop over the same parts of the fronts, we could instead iterate over both fronts in sequence + for depth in range(highest_depth + 1): + res1 = front1.get_smallest_at_depth(depth) + res2 = front2.get_smallest_at_depth(depth) + + if res1 is None or res2 is None: + continue + + d1, _, _ = res1 + d2, _, _ = res2 + + if last_depth is None or d1 > last_depth or d2 > last_depth: + yield res1, res2 + + +class SizeParetoFront(ParetoFront): + """A `ParetoFront` that trades off multiplicative depth with multiplicative size.""" + + def _get_value(self, node: "ArithmeticNode") -> int: + return node.multiplicative_size() + + def _default_value(self) -> int: + return 0 + + def add(self, node: "ArithmeticNode", depth: Optional[int] = None, size: Optional[int] = None): + """Adds the given `Node` to the `SizeParetoFront` by computing its multiplicative depth and size. + + Alternatively, the user can supply an unchecked `depth` and `size` so that these values do not have to be (re)computed. + + Returns: + `True` if and only if the node was inserted into the ParetoFront (so it was in some way better than the current `Nodes`). + """ + return super().add(node, depth, value=size) + + +class CostParetoFront(ParetoFront): + """A `ParetoFront` that trades off multiplicative depth with multiplicative cost.""" + + def __init__(self, cost_of_squaring: float) -> None: + """Initialize an empty `CostParetoFront` with the given `cost_of_squaring`.""" + self._cost_of_squaring = cost_of_squaring + super().__init__() + + @classmethod + def from_node( + cls, + node: "ArithmeticNode", + cost_of_squaring: float, + depth: Optional[int] = None, + cost: Optional[float] = None, + ) -> "CostParetoFront": + """Initialize a `CostParetoFront` with one node in it. + + Returns: + New `CostParetoFront`. + """ + self = cls(cost_of_squaring) + self.add(node, depth, cost) + return self + + @classmethod + def from_leaf(cls, leaf, cost_of_squaring: float) -> "CostParetoFront": + """Initialize a `CostParetoFront` with one leaf node in it. + + Returns: + New `CostParetoFront`. + """ + self = cls(cost_of_squaring) + self.add_leaf(leaf) + return self + + def _get_value(self, node: "ArithmeticNode") -> float: + return node.multiplicative_cost(self._cost_of_squaring) + + def _default_value(self) -> float: + return 0.0 + + def add( + self, node: "ArithmeticNode", depth: Optional[int] = None, cost: Optional[float] = None + ) -> bool: + """Adds the given `Node` to the `CostParetoFront` by computing its multiplicative depth and cost. + + Alternatively, the user can supply an unchecked `depth` and `cost` so that these values do not have to be (re)computed. + + Returns: + `True` if and only if the node was inserted into the ParetoFront (so it was in some way better than the current `Nodes`). + """ + return super().add(node, depth, value=cost) + + +def _to_node(obj: Union["Node", int, bool], gf: Type[FieldArray]) -> "Node": + if isinstance(obj, Node): + return obj + + if isinstance(obj, int): + from oraqle.compiler.nodes.leafs import Constant + + return Constant(gf(obj)) + + +def try_to_node(obj: Any, gf: Type[FieldArray]) -> Optional["Node"]: + """Tries to cast this object into a valid `Node`. + + This can be used to transform e.g. an `int` or `bool` into a `Constant`. + If it is applied to a `Node`, it does nothing. + + Returns: + A `Node` or `None` depending on whether the object is castable. + """ + return _to_node(obj, gf) + + +class Node(ABC): # noqa: PLR0904 + """Abstract node in an arithmetic circuit.""" + + @property + @abstractmethod + def _node_label(self) -> str: + pass + + # TODO: This property should be removed if we do not provide a default hash implementation. + @property + @abstractmethod + def _hash_name(self) -> str: + pass + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"style": "rounded,filled", "fillcolor": "cornsilk"} + + def __init__(self, gf: Type[FieldArray]): + """Creates a new node, of which the result is known by the parties identified by `known_by`, as well as those who know all input operands.""" + # TODO: We should probably make separate methods to clear individual caches + self._evaluate_cache: Optional[FieldArray] = None + self._to_graph_cache: Optional[int] = None + self._arithmetize_cache: Optional[Node] = None + self._arithmetize_depth_cache: Optional[CostParetoFront] = None + self._instruction_cache: Optional[int] = None + self._arithmetic_cache: Optional[ArithmeticNode] = None + self._parent_count_cache: Optional[int] = None + + self._hash = None + + self._party = None + self._plaintext = False + self._parent_count = 0 + + self._gf = gf + + @abstractmethod + def apply_function_to_operands(self, function: Callable[["Node"], None]): + """Applies function to all operands of this node.""" + + @abstractmethod + def replace_operands_using_function(self, function: Callable[["Node"], "Node"]): + """Replaces each operand of this node with the node generated by calling function on said operand.""" + + @abstractmethod + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> FieldArray: + """Evaluates the node in the arithmetic circuit. The output should always be reduced modulo the modulus.""" + + def clear_cache(self, already_cleared: Set[int]): + """Clears any cached values of the node and any of its operands.""" + # FIXME: The cache should not be cleared twice for the same node, but there is no way to check this. + if id(self) not in already_cleared: + self.apply_function_to_operands(lambda operand: operand.clear_cache(already_cleared)) + + self._evaluate_cache: Optional[FieldArray] = None + self._to_graph_cache: Optional[int] = None + self._arithmetize_cache: Optional[Node] = None + self._arithmetize_depth_cache: Optional[CostParetoFront] = None + self._instruction_cache: Optional[int] = None + self._arithmetic_cache: Optional[ArithmeticNode] = None + self._parent_count_cache: Optional[int] = None + + self._hash = None + + already_cleared.add(id(self)) + + def to_graph(self, graph_builder: DotFile) -> int: + """Adds this node to the graph as well as its edges. + + Returns: + The identifier of this `Node` in the `DotFile`. + """ + if self._to_graph_cache is None: + attributes = {"shape": "box"} + attributes.update(self._overriden_graphviz_attributes) + + self._to_graph_cache = graph_builder.add_node( + label=self._node_label, + **attributes, + ) + + # FIXME: This does not take multiplicity into account; add option to apply_function_to_operands to take multiplicity into account + self.apply_function_to_operands(lambda operand: graph_builder.add_link(operand.to_graph(graph_builder), self._to_graph_cache)) # type: ignore + + return self._to_graph_cache + + @abstractmethod + def __hash__(self) -> int: + raise NotImplementedError( + "The abstract class does not provide a default implementation of __hash__" + ) + + # TODO: We can add a strategy to this method, e.g. to exhaustively check equivalence. + @abstractmethod + def is_equivalent(self, other: "Node") -> bool: + """Checks whether two nodes are semantically equivalent. + + This method will always return `False` if they are not. + This method will maybe return True if they are indeed equivalent. + In other words, this method may produce false negatives, but it will never produce false positives. + """ + + # TODO: Rework CSE. In an arithmetic circuit, it should only return arithmetic nodes. + def eliminate_common_subexpressions(self, terms: Dict[int, "Node"]) -> "Node": + """Eliminates duplicate subexpressions that are equivalent (as defined by a node's `__eq__` and `__hash__` method). + + Returns: + A `Node` that must replace the previous expression. + """ + # TODO: What if we try breadth-first search? It will be more expensive but it will save the lowest depth solution first. + # FIXME: Handle conflicts (duplicate hashes) using a list instead of a single node. + # TODO: For performance reasons, maybe we should only save terms of a certain maximum depth. + h = hash(self) + if h in terms and self.is_equivalent(terms[h]): + return terms[h] + + self.replace_operands_using_function( + lambda operand: operand.eliminate_common_subexpressions(terms) + ) + + terms[h] = self + return self + + def count_parents(self): + """Counts the total number of nodes in this subcircuit.""" + self._parent_count += 1 + + if self._parent_count_cache is None: + self._parent_count_cache = True + self.apply_function_to_operands(lambda operand: operand.count_parents()) + + def reset_parent_count(self): + """Resets the cached number of nodes in this subcircuit to 0.""" + self._parent_count = 0 + self.apply_function_to_operands(lambda operand: operand.reset_parent_count()) + + @abstractmethod + def arithmetize(self, strategy: str) -> "Node": + """Arithmetizes this node, replacing it with only arithmetic operations (constants, additions, and multiplications). + + The current implementation only aims at reducing the total number of multiplications. + """ + + @abstractmethod + def arithmetize_depth_aware( + self, cost_of_squaring: float + ) -> "CostParetoFront": + """Arithmetizes this node in a depth-aware fashion, replacing high-level nodes with only arithmetic operations (constants, additions, and multiplications). + + Returns: + `CostParetoFront` containing a front that trades off multiplicative depth and multiplicative cost. + """ + + def to_arithmetic(self) -> "ArithmeticNode": + """Outputs this node's equivalent ArithmeticNode. Errors if this node does not have a direct arithmetic equivalent. + + Raises: + Exception: If there is no direct arithmetic equivalent. + """ + # TODO: Make this a non-generic exception + raise Exception( + f"This node does not have a direct arithmetic equivalent: {self}. Consider first calling `arithmetize`." + ) + + def add(self, other: "Node", flatten=True) -> "Node": + """Performs a summation between `self` and `other`, possibly flattening any sums. + + It is possible to disable flattening by setting `flatten=False`. + + Returns: + A possibly flattened `Sum` node or a `Constant` representing self & other. + """ + from oraqle.compiler.nodes.arbitrary_arithmetic import Sum + from oraqle.compiler.nodes.leafs import Constant + + if flatten and isinstance(self, Sum): + return self.add_flatten(other) + + if flatten and isinstance(other, Sum): + return other.add_flatten(self) + + if isinstance(other, Constant): + if int(other._value) == 0: + return self + return Sum(Counter({UnoverloadedWrapper(self): 1}), self._gf, constant=other._value) + + if id(self) == id(other): + return Sum(Counter({UnoverloadedWrapper(self): 2}), self._gf) + else: + return Sum( + Counter({UnoverloadedWrapper(self): 1, UnoverloadedWrapper(other): 1}), self._gf + ) + + def __add__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this + cannot be made into a Node: {self} - {other}") + + return self.add(other_node) + + def __radd__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The LHS of this + cannot be made into a Node: {other} - {self}") + + return self.add(other_node) + + def mul(self, other: "Node", flatten=True) -> "Node": # noqa: PLR0911 + """Performs a multiplication between `self` and `other`, possibly flattening any products. + + It is possible to disable flattening by setting `flatten=False`. + + Returns: + A possibly flattened `Product` node or a `Constant` representing self & other. + """ + from oraqle.compiler.nodes.arbitrary_arithmetic import Product + from oraqle.compiler.nodes.leafs import Constant + + if flatten and isinstance(self, Product): + return self.mul_flatten(other) + + if flatten and isinstance(other, Product): + return other.mul_flatten(self) + + if isinstance(other, Constant): + if int(other._value) == 0: + return other + if int(other._value) == 1: + return self + return Product(Counter({UnoverloadedWrapper(self): 1}), self._gf, constant=other._value) + + if id(self) == id(other): + return Product(Counter({UnoverloadedWrapper(self): 2}), self._gf) + else: + return Product( + Counter({UnoverloadedWrapper(self): 1, UnoverloadedWrapper(other): 1}), self._gf + ) + + def __mul__(self, other) -> "Node": + if not isinstance(other, Node): + raise Exception(f"The RHS of this multiplication is not a Node: {self} * {other}") + + return self.mul(other) + + def bool_or(self, other: "Node", flatten=True) -> "Node": + """Performs an OR operation between `self` and `other`, possibly flattening the result into an OR operation between many operands. + + It is possible to disable flattening by setting `flatten=False`. + + Returns: + A possibly flattened `Or` node or a `Constant` representing self & other. + """ + from oraqle.compiler.boolean.bool_or import Or + from oraqle.compiler.nodes.leafs import Constant + + if flatten and isinstance(other, Or): + return other.or_flatten(self) + + if isinstance(other, Constant): + if bool(other._value): + return Constant(self._gf(1)) + else: + return self + + if self.is_equivalent(other): + return self + else: + return Or({UnoverloadedWrapper(self), UnoverloadedWrapper(other)}, self._gf) + + def __or__(self, other) -> "Node": + if not isinstance(other, Node): + raise Exception(f"The RHS of this OR is not a Node: {self} | {other}") + + return self.bool_or(other) + + def bool_and(self, other: "Node", flatten=True) -> "Node": + """Performs an AND operation between `self` and `other`, possibly flattening the result into an AND operation between many operands. + + It is possible to disable flattening by setting `flatten=False`. + + Returns: + A possibly flattened `And` node or a `Constant` representing self & other. + """ + from oraqle.compiler.boolean.bool_and import And + from oraqle.compiler.nodes.leafs import Constant + + if flatten and isinstance(other, And): + return other.and_flatten(self) + + if isinstance(other, Constant): + if bool(other._value): + return self + else: + return Constant(self._gf(0)) + + if self.is_equivalent(other): + return self + else: + return And({UnoverloadedWrapper(self), UnoverloadedWrapper(other)}, self._gf) + + def __and__(self, other) -> "Node": + if not isinstance(other, Node): + raise Exception(f"The RHS of this AND is not a Node: {self} & {other}") + + return self.bool_and(other) + + def __lt__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this < cannot be made into a Node: {self} < {other}") + + from oraqle.compiler.comparison.comparison import StrictComparison + + return StrictComparison(self, other_node, less_than=True, gf=self._gf) + + def __gt__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this > cannot be made into a Node: {self} > {other}") + + from oraqle.compiler.comparison.comparison import StrictComparison + + return StrictComparison(self, other_node, less_than=False, gf=self._gf) + + def __le__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this <= cannot be made into a Node: {self} <= {other}") + + from oraqle.compiler.comparison.comparison import Comparison + + return Comparison(self, other_node, less_than=True, gf=self._gf) + + def __ge__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this >= cannot be made into a Node: {self} >= {other}") + + from oraqle.compiler.comparison.comparison import Comparison + + return Comparison(self, other_node, less_than=False, gf=self._gf) + + def __neg__(self) -> "Node": + from oraqle.compiler.nodes.leafs import Constant + + return Constant(-self._gf(1)) * self + + def __invert__(self) -> "Node": + from oraqle.compiler.boolean.bool_neg import Neg + + return Neg(self, self._gf) + + def __pow__(self, other) -> "Node": + if not isinstance(other, int): + raise Exception(f"The exponent must be an integer: {self}**{other}") + + from oraqle.compiler.arithmetic.exponentiation import Power + + return Power(self, other, self._gf) + + def __sub__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this - cannot be made into a Node: {self} - {other}") + + from oraqle.compiler.arithmetic.subtraction import Subtraction + + return Subtraction(self, other_node, self._gf) + + def __rsub__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The LHS of this - cannot be made into a Node: {other} - {self}") + + from oraqle.compiler.arithmetic.subtraction import Subtraction + + return Subtraction(other_node, self, self._gf) + + def __eq__(self, other) -> "Node": + other_node = try_to_node(other, self._gf) + if other_node is None: + raise Exception(f"The RHS of this == cannot be made into a Node: {self} == {other}") + + from oraqle.compiler.comparison.equality import Equals + + return Equals(self, other_node, self._gf) + + +class UnoverloadedWrapper: + """The `UnoverloadedWrapper` class wraps a `Node` such that hash(.) and x == y work as expected. + + !!! note + The equality operator perform semantic equality! + """ + + def __init__(self, node: Node) -> None: + """Wrap `Node`.""" + self.node = node + + def __hash__(self) -> int: + return hash(self.node) + + def __eq__(self, other) -> bool: + if not isinstance(other, UnoverloadedWrapper): + return False + + if hash(self) != hash(other): + return False + + return self.node.is_equivalent(other.node) + + +# TODO: Do we need a separate class to distinguish nodes from arithmetic nodes (which only have arithmetic operands)? +class ArithmeticNode(Node): + """Extension of Node to indicate that this is a node permitted in a purely arithmetic circuit (with binary additions and multiplications). + + The ArithmeticNode 'mixin' must always come before the base class in the class declaration. + """ + + # ArithmeticNode should be like an interface; it should not have an __init__ method. + + def clear_cache(self, already_cleared: Set[int]): + """Clears any cached values of the node and any of its operands.""" + # FIXME: The cache should not be cleared twice for the same node, but there is no way to check this. + if id(self) not in already_cleared: + for node in self.operands(): + node.clear_cache(already_cleared) + + self._evaluate_cache: Optional[FieldArray] = None + self._to_graph_cache: Optional[int] = None + self._arithmetize_cache: Optional[Node] = None + self._arithmetize_depth_cache: Optional[ParetoFront] = None + self._instruction_cache: Optional[int] = None + self._arithmetic_cache: Optional[ArithmeticNode] = None + self._parent_count_cache: Optional[int] = None + + self._hash = None + + already_cleared.add(id(self)) + + @abstractmethod + def operands(self) -> List["ArithmeticNode"]: + """Returns the operands (children) of this node. The list can be empty. The nodes MUST be arithmetic nodes.""" + + @abstractmethod + def set_operands(self, operands: List["ArithmeticNode"]): + """Overwrites the operands of this node. The nodes MUST be arithmetic nodes.""" + + @abstractmethod + def multiplicative_depth(self) -> int: + """Computes the multiplicative depth of this node and its children recursively. + + Returns: + The largest number of multiplications from the output of this node to the leafs of this subcircuit. + """ + + def multiplicative_size(self) -> int: + """Computes the multiplicative size (number of multiplications) by counting the size of the set returned by self.multiplications(). + + Returns: + The number of multiplications in this subcircuit. + """ + return len(self.multiplications()) + + def multiplicative_cost(self, cost_of_squaring: float) -> float: + """Computes the multiplicative cost (number of general multiplications + cost_of_squaring * squarings). + + It does so by counting the size of the sets returned by self.multiplications() and self.squarings(). + + Returns: + The number of proper multiplications + the cost of squaring * the number of squarings. + """ + return ( + len(self.multiplications()) + - len(self.squarings()) + + cost_of_squaring * len(self.squarings()) + ) + + @abstractmethod + def multiplications(self) -> Set[int]: + """Returns a set of all the multiplications in this tree of descendants, including itself. + + This includes any squarings. + """ + + @abstractmethod + def squarings(self) -> Set[int]: + """Returns a set of all the squarings in this tree of descendants, including itself.""" + + def arithmetize(self, strategy: str) -> "ArithmeticNode": # noqa: D102 + if self._arithmetize_cache2 is None: + self.set_operands([operand.arithmetize(strategy) for operand in self.operands()]) + self._arithmetize_cache2 = self + + return self._arithmetize_cache2 + + @abstractmethod + def create_instructions( + self, + instructions: List[ArithmeticInstruction], + stack_counter: int, + stack_occupied: List[bool], + ) -> Tuple[int, int]: + """Creates a set of instructions of this node to the given file. Returns the index in the stack of the output and the stack_counter. + + !!! note + This method assumes that the _parent_count of each node is up to date. + """ + + def to_arithmetic(self) -> "ArithmeticNode": # noqa: D102 + return self diff --git a/oraqle/compiler/nodes/arbitrary_arithmetic.py b/oraqle/compiler/nodes/arbitrary_arithmetic.py new file mode 100644 index 0000000..d2516a9 --- /dev/null +++ b/oraqle/compiler/nodes/arbitrary_arithmetic.py @@ -0,0 +1,392 @@ +"""This module contains arithmetic operations between a flexible number of inputs: summations and products.""" +import itertools +from collections import Counter +from dataclasses import dataclass, field +from functools import reduce +from heapq import heapify, heappop, heappush +from typing import Any +from typing import Counter as CounterType +from typing import Dict, Iterable, Optional, Tuple, Type, Union + +from galois import FieldArray + +from oraqle.compiler.nodes.abstract import ( + ArithmeticNode, + CostParetoFront, + Node, + UnoverloadedWrapper, + _to_node, +) +from oraqle.compiler.nodes.binary_arithmetic import Addition, Multiplication +from oraqle.compiler.nodes.flexible import CommutativeMultiplicityReducibleNode +from oraqle.compiler.nodes.leafs import Constant +from oraqle.compiler.nodes.unary_arithmetic import ConstantAddition, ConstantMultiplication + + +# TODO: This is mostly copied from generate_multiplication_tree (depth is different) +def _generate_addition_tree( + summands: Iterable[Tuple[int, ArithmeticNode]], counts: Iterable[int] +) -> Tuple[int, Addition]: + queue = [ + _PrioritizedItem(*summand) for summand, count in zip(summands, counts) for _ in range(count) + ] + heapify(queue) + + while len(queue) > 1: + a = heappop(queue) + b = heappop(queue) + + a_const = isinstance(a.item, Constant) + b_const = isinstance(b.item, Constant) + + # TODO: This should move to Node + if a_const: + if b_const: + new = a.item + b.item + else: + new = b.item if a.item._value == 0 else ConstantAddition(b.item, a.item._value) + elif b_const: + new = a.item if b.item._value == 0 else ConstantAddition(a.item, b.item._value) + else: + new = Addition(a.item, b.item, a.item._gf) + + heappush( + queue, + _PrioritizedItem(max(a.priority, b.priority), new), + ) + + return (queue[0].priority, queue[0].item) + + +class Sum(CommutativeMultiplicityReducibleNode): + """This node represents a sum between two or more operands, or at least one operand and a constant.""" + + @property + def _hash_name(self) -> str: + return "sum" + + @property + def _node_label(self) -> str: + return "+" + + @property + def _identity(self) -> FieldArray: + return self._gf(0) + + def _arithmetize_inner(self, strategy: str) -> Node: + # TODO: Wrap exponents + new_operands = Counter() + new_constant = self._constant + for operand, count in self._operands.items(): + new_operand = operand.node.arithmetize(strategy) + + if isinstance(new_operand, Constant): + new_constant += new_operand._value * count + else: + new_operands[UnoverloadedWrapper(new_operand)] += count + + if len(new_operands) == 0: + return Constant(new_constant) # type: ignore + elif sum(new_operands.values()) == 1 and new_constant == self._identity: + return next(iter(new_operands)).node + + return Sum(new_operands, self._gf, new_constant) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + # FIXME: This could be done way more efficiently by iterating over increasing depth + front = CostParetoFront(cost_of_squaring) + + for operands in itertools.product( + *( + operand.node.arithmetize_depth_aware(cost_of_squaring) + for operand in self._operands + ) + ): + addition_tree = _generate_addition_tree( + ((d, operand) for d, _, operand in operands), self._operands.values() + ) + if self._constant != self._identity: + if isinstance(addition_tree[1], Constant): + return CostParetoFront.from_leaf( + Constant(addition_tree[1]._value + self._constant), cost_of_squaring + ) + + addition_tree = ( + addition_tree[0], + ConstantAddition(addition_tree[1], self._constant), + ) + front.add(addition_tree[1], depth=addition_tree[0]) + + assert not front.is_empty() + return front + + def to_arithmetic(self) -> ArithmeticNode: # noqa: D102 + if self._arithmetic_cache is None: + # FIXME: Perform actual rebalancing + operands = iter(self._operands.elements()) + + # TODO: There is a lot of duplication between this and multiplications + if self._constant == self._identity: + self._arithmetic_cache = Addition( + next(operands).node.to_arithmetic(), + next(operands).node.to_arithmetic(), + self._gf, + ) + else: + self._arithmetic_cache = ConstantAddition( + next(operands).node.to_arithmetic(), self._constant + ) + + for operand in operands: + self._arithmetic_cache = Addition( + self._arithmetic_cache, operand.node.to_arithmetic(), self._gf + ) + + return self._arithmetic_cache + + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> FieldArray: # noqa: D102 + if self._evaluate_cache is None: + self._evaluate_cache = reduce( + lambda a, b: a + b, + ( + operand.node.evaluate(actual_inputs) * count + for operand, count in self._operands.items() + ), + ) + self._evaluate_cache += self._constant + + return self._evaluate_cache # type: ignore + + def add_flatten(self, other: Node) -> Node: + """Adds this node to `other`, flattening the summation if either of the two is also a `Sum` and absorbing `Constant`s. + + Returns: + A `Sum` node containing the flattened summation, or a `Constant` node. + """ + order = self._gf.order + # TODO: Consider already assigning values to e.g. result._depth + if isinstance(other, Sum): + counter = self._operands + other._operands + counter_dict = { + el: count % order for el, count in counter.items() if count % order != 0 + } + constant = self._constant + other._constant + if len(counter_dict) == 0: + return Constant(constant) # type: ignore + return Sum(Counter(counter_dict), self._gf, constant) # type: ignore + elif isinstance(other, Constant): + if sum(self._operands.values()) == 1 and int(self._constant + other._value) == 0: + return next(iter(self._operands)).node + return Sum(self._operands, self._gf, self._constant + other._value) # type: ignore + + counter = self._operands.copy() + unoverloaded_other = UnoverloadedWrapper(other) + counter[unoverloaded_other] = (counter[unoverloaded_other] + 1) % order + if counter[unoverloaded_other] == 0: + counter.pop(unoverloaded_other) + + # FIXME: If empty, return Constant(0) + + return Sum(counter, self._gf, self._constant) + + +@dataclass(order=True) +class _PrioritizedItem: + priority: int + item: Any = field(compare=False) + + +def _generate_multiplication_tree( + multiplicands: Iterable[Tuple[int, ArithmeticNode]], counts: Iterable[int] +) -> Tuple[int, Multiplication]: + queue = [ + _PrioritizedItem(*multiplicand) + for multiplicand, count in zip(multiplicands, counts) + for _ in range(count) + ] + heapify(queue) + + while len(queue) > 1: + a = heappop(queue) + b = heappop(queue) + + a_const = isinstance(a.item, Constant) + b_const = isinstance(b.item, Constant) + + # TODO: This should move to Node + if a_const: + if b_const: + new = a.item * b.item + elif a.item._value == 1: + new = b.item + else: + new = ConstantMultiplication(b.item, a.item._value) + elif b_const: + new = a.item if b.item._value == 1 else ConstantMultiplication(a.item, b.item._value) + else: + new = Multiplication(a.item, b.item, a.item._gf) + + heappush( + queue, + _PrioritizedItem(max(a.priority, b.priority) + (not a_const and not b_const), new), + ) + + return (queue[0].priority, queue[0].item) + + +class Product(CommutativeMultiplicityReducibleNode): + """This node represents a product between two or more operands, or at least one operand and a constant.""" + + def __init__( + self, + operands: CounterType[UnoverloadedWrapper], + gf: Type[FieldArray], + constant: Optional[FieldArray] = None, + ): + """Initialize a `Product` with the given `Counter` as operands and an optional `constant`.""" + super().__init__(operands, gf, constant) + assert constant != 0 + + @property + def _hash_name(self) -> str: + return "product" + + @property + def _node_label(self) -> str: + return "ร—" # noqa: RUF001 + + @property + def _identity(self) -> FieldArray: + return self._gf(1) + + def _inner_operation(self, a: FieldArray, b: FieldArray) -> FieldArray: + return a * b # type: ignore + + def _arithmetize_inner(self, strategy: str) -> Node: + # TODO: Wrap exponents + new_operands = Counter() + new_constant = self._constant + for operand, count in self._operands.items(): + new_operand = operand.node.arithmetize(strategy) + + if isinstance(new_operand, Constant): + new_constant *= new_operand._value**count + else: + new_operands[UnoverloadedWrapper(new_operand)] += count + + if len(new_operands) == 0: + return Constant(new_constant) # type: ignore + elif sum(new_operands.values()) == 1 and new_constant == self._identity: + return next(iter(new_operands)).node + + if new_constant == 0: + return Constant(self._gf(0)) + + return Product(new_operands, self._gf, new_constant) # type: ignore + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + # TODO: This could be done more efficiently by going breadth-wise + front = CostParetoFront(cost_of_squaring) + + for operands in itertools.product( + *( + operand.node.arithmetize_depth_aware(cost_of_squaring) + for operand in self._operands + ) + ): + multiplication_tree = _generate_multiplication_tree( + ((d, operand) for d, _, operand in operands), self._operands.values() + ) + if self._constant != self._identity: + if isinstance(multiplication_tree[1], Constant): + return CostParetoFront.from_leaf( + Constant(multiplication_tree[1]._value * self._constant), cost_of_squaring + ) + + multiplication_tree = ( + multiplication_tree[0], + ConstantMultiplication(multiplication_tree[1], self._constant), + ) + front.add(multiplication_tree[1], depth=multiplication_tree[0]) + + assert not front.is_empty() + return front + + def to_arithmetic(self) -> ArithmeticNode: # noqa: D102 + if self._arithmetic_cache is None: + # FIXME: Perform actual rebalancing + operands = iter(self._operands.elements()) + + if self._constant == self._identity: + self._arithmetic_cache = Multiplication( + next(operands).node.to_arithmetic(), + next(operands).node.to_arithmetic(), + self._gf, + ) + else: + self._arithmetic_cache = ConstantMultiplication( + next(operands).node.to_arithmetic(), self._constant + ) + + for operand in operands: + self._arithmetic_cache = Multiplication( + self._arithmetic_cache, operand.node.to_arithmetic(), self._gf + ) + + return self._arithmetic_cache + + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> FieldArray: # noqa: D102 + if self._evaluate_cache is None: + self._evaluate_cache = reduce(lambda a, b: a * b, (operand.node.evaluate(actual_inputs) ** count for operand, count in self._operands.items())) # type: ignore + self._evaluate_cache *= self._constant # type: ignore + + return self._evaluate_cache # type: ignore + + def mul_flatten(self, other: Node) -> Node: + """Multiplies this node with `other`, flattening the product if either of the two is also a `Product` and absorbing `Constant`s. + + Returns: + A `Product` node containing the flattened product, or a `Constant` node. + """ + # TODO: Consider already assigning values to e.g. result._depth + if isinstance(other, Product): + # TODO: Wrap powers (due to modulo arithmetic) + return Product(self._operands + other._operands, self._gf, self._constant * other._constant) # type: ignore + elif isinstance(other, Constant): + if other._value == 0: + return Constant(self._gf(0)) + return Product(self._operands, self._gf, self._constant * other._value) # type: ignore + + counter = self._operands.copy() + counter[UnoverloadedWrapper(other)] += 1 # type: ignore + return Product(counter, self._gf, self._constant) + + +def _first_gf(*operands: Union[Node, int, bool]) -> Optional[Type[FieldArray]]: + for operand in operands: + if isinstance(operand, Node): + return operand._gf + + +def sum_(*operands: Union[Node, int, bool]) -> Sum: + """Performs a sum between any number of nodes (or operands such as integers). + + Returns: + A `Sum` between all operands. + """ + assert len(operands) > 0 + gf = _first_gf(*operands) + assert gf is not None + return Sum(Counter(UnoverloadedWrapper(_to_node(operand, gf)) for operand in operands), gf) + + +def product_(*operands: Node) -> Product: + """Performs a product between any number of nodes (or operands such as integers). + + Returns: + A `Product` between all operands. + """ + assert len(operands) > 0 + gf = _first_gf(*operands) + assert gf is not None + return Product(Counter(UnoverloadedWrapper(_to_node(operand, gf)) for operand in operands), gf) diff --git a/oraqle/compiler/nodes/binary_arithmetic.py b/oraqle/compiler/nodes/binary_arithmetic.py new file mode 100644 index 0000000..ec5daac --- /dev/null +++ b/oraqle/compiler/nodes/binary_arithmetic.py @@ -0,0 +1,264 @@ +"""Module containing binary arithmetic nodes: additions and multiplications between non-constant nodes.""" +from abc import abstractmethod +from typing import List, Optional, Set, Tuple, Type + +from galois import FieldArray + +from oraqle.compiler.instructions import ( + AdditionInstruction, + ArithmeticInstruction, + MultiplicationInstruction, +) +from oraqle.compiler.nodes.abstract import ( + ArithmeticNode, + CostParetoFront, + Node, + iterate_increasing_depth, + select_stack_index, +) +from oraqle.compiler.nodes.fixed import BinaryNode +from oraqle.compiler.nodes.leafs import Constant + + +class CommutativeBinaryNode(BinaryNode): + """This node has two operands and implements a commutative operation between arithmetic nodes.""" + + def __init__( + self, + left: Node, + right: Node, + gf: Type[FieldArray], + ): + """Initialize the binary node with operands `left` and `right`.""" + self._left = left + self._right = right + super().__init__(gf) + + @abstractmethod + def _operation_inner(self, x: FieldArray, y: FieldArray) -> FieldArray: + """Applies the binary operation on x and y.""" + + def operation(self, operands: List[FieldArray]) -> FieldArray: # noqa: D102 + return self._operation_inner(operands[0], operands[1]) + + def operands(self) -> List[Node]: # noqa: D102 + return [self._left, self._right] + + def set_operands(self, operands: List[ArithmeticNode]): # noqa: D102 + self._left = operands[0] + self._right = operands[1] + + def __hash__(self) -> int: + if self._hash is None: + left_hash = hash(self._left) + right_hash = hash(self._right) + + # Make the hash commutative + if left_hash < right_hash: + self._hash = hash((self._hash_name, (left_hash, right_hash))) + else: + self._hash = hash((self._hash_name, (right_hash, left_hash))) + + return self._hash + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + if hash(self) != hash(other): + return False + + # Equivalence by commutative equality + return ( + self._left.is_equivalent(other._left) and self._right.is_equivalent(other._right) + ) or (self._left.is_equivalent(other._right) and self._right.is_equivalent(other._left)) + + +class CommutativeArithmeticBinaryNode(CommutativeBinaryNode): + """This node has two operands and implements a commutative operation between arithmetic nodes.""" + + def __init__( + self, + left: ArithmeticNode, + right: ArithmeticNode, + gf: Type[FieldArray], + ): + """Initialize this binary node with the given `left` and `right` operands. + + Raises: + Exception: Neither `left` nor `right` is allowed to be a `Constant`. + """ + super().__init__(left, right, gf) + + self._multiplications: Optional[Set[int]] = None + self._squarings: Optional[Set[int]] = None + self._depth_cache: Optional[int] = None + + if isinstance(left, Constant) or isinstance(right, Constant): + self._is_multiplication = False + raise Exception("This should be a constant.") + + def multiplicative_depth(self) -> int: # noqa: D102 + if self._depth_cache is None: + self._depth_cache = self._is_multiplication + max( + self._left.multiplicative_depth(), self._right.multiplicative_depth() + ) + + return self._depth_cache + + def multiplications(self) -> Set[int]: # noqa: D102 + if self._multiplications is None: + self._multiplications = set().union( + *(operand.multiplications() for operand in self.operands()) # type: ignore + ) + if self._is_multiplication: + self._multiplications.add(id(self)) + + return self._multiplications + + # TODO: Squaring should probably be a UniveriateNode + def squarings(self) -> Set[int]: # noqa: D102 + if self._squarings is None: + self._squarings = set().union(*(operand.squarings() for operand in self.operands())) # type: ignore + if self._is_multiplication and id(self._left) == id(self._right): + self._squarings.add(id(self)) + + return self._squarings + + def create_instructions( # noqa: D102 + self, + instructions: List[ArithmeticInstruction], + stack_counter: int, + stack_occupied: List[bool], + ) -> Tuple[int, int]: + self._left: ArithmeticNode + self._right: ArithmeticNode + + if self._instruction_cache is None: + left_index, stack_counter = self._left.create_instructions( + instructions, stack_counter, stack_occupied + ) + right_index, stack_counter = self._right.create_instructions( + instructions, stack_counter, stack_occupied + ) + + # FIXME: Is it possible for e.g. self._left._instruction_cache to be None? + + self._left._parent_count -= 1 + if self._left._parent_count == 0: + stack_occupied[self._left._instruction_cache] = False # type: ignore + + self._right._parent_count -= 1 + if self._right._parent_count == 0: + stack_occupied[self._right._instruction_cache] = False # type: ignore + + self._instruction_cache = select_stack_index(stack_occupied) + + if self._is_multiplication: + instructions.append( + MultiplicationInstruction(self._instruction_cache, left_index, right_index) + ) + else: + instructions.append( + AdditionInstruction(self._instruction_cache, left_index, right_index) + ) + + return self._instruction_cache, stack_counter + + +# FIXME: This order should probably change +class Addition(CommutativeArithmeticBinaryNode, ArithmeticNode): + """Performs modular addition of two previous nodes in an arithmetic circuit.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"shape": "square", "style": "rounded,filled", "fillcolor": "grey80"} + + @property + def _hash_name(self) -> str: + return "add" + + @property + def _node_label(self) -> str: + return "+" + + def __init__( + self, + left: ArithmeticNode, + right: ArithmeticNode, + gf: Type[FieldArray], + ): + """Initialize a modular addition between `left` and `right`.""" + self._is_multiplication = False + super().__init__(left, right, gf) + + def _operation_inner(self, x, y): + return x + y + + def arithmetize(self, strategy: str) -> Node: # noqa: D102 + self._left = self._left.arithmetize(strategy) + self._right = self._right.arithmetize(strategy) + return self + + def _arithmetize_inner(self, strategy: str) -> Node: + raise NotImplementedError() + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + front = CostParetoFront(cost_of_squaring) + + for res1, res2 in iterate_increasing_depth( + self._left.arithmetize_depth_aware(cost_of_squaring), + self._right.arithmetize_depth_aware(cost_of_squaring), + ): + d1, _, e1 = res1 + d2, _, e2 = res2 + + # TODO: Do we use + here for flattening? + front.add(Addition(e1, e2, self._gf), depth=max(d1, d2)) + + assert not front.is_empty() + return front + + +class Multiplication(CommutativeArithmeticBinaryNode, ArithmeticNode): + """Performs modular multiplication of two previous nodes in an arithmetic circuit.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"shape": "square", "style": "rounded,filled", "fillcolor": "lightpink"} + + @property + def _hash_name(self) -> str: + return "mul" + + @property + def _node_label(self) -> str: + return "ร—" # noqa: RUF001 + + def __init__( + self, + left: ArithmeticNode, + right: ArithmeticNode, + gf: Type[FieldArray], + ): + """Initialize a modular multiplication between `left` and `right`.""" + assert isinstance(left, ArithmeticNode) + assert isinstance(right, ArithmeticNode) + + self._is_multiplication = True + super().__init__(left, right, gf) + + def _operation_inner(self, x, y): + return x * y + + # TODO: This is very hacky! Arithmetic nodes should simply not have to be arithmetized... + def arithmetize(self, strategy: str) -> Node: # noqa: D102 + self._left = self._left.arithmetize(strategy) + self._right = self._right.arithmetize(strategy) + return self + + def _arithmetize_inner(self, strategy: str) -> Node: + raise NotImplementedError() + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return CostParetoFront.from_node(self, cost_of_squaring) diff --git a/oraqle/compiler/nodes/fixed.py b/oraqle/compiler/nodes/fixed.py new file mode 100644 index 0000000..767d403 --- /dev/null +++ b/oraqle/compiler/nodes/fixed.py @@ -0,0 +1,100 @@ +"""Module containing fixed nodes: nodes with a fixed number of inputs.""" +from abc import abstractmethod +from typing import Callable, Dict, List + +from galois import FieldArray + +from oraqle.compiler.nodes.abstract import CostParetoFront, Node + + +class FixedNode(Node): + """A node with a fixed number of operands.""" + + @abstractmethod + def operands(self) -> List["Node"]: + """Returns the operands (children) of this node. The list can be empty.""" + + @abstractmethod + def set_operands(self, operands: List["Node"]): + """Overwrites the operands of this node.""" + # TODO: Consider replacing this method with a graph traversal method that applies a function on all operands and replaces them. + + + def apply_function_to_operands(self, function: Callable[[Node], None]): # noqa: D102 + for operand in self.operands(): + function(operand) + + + def replace_operands_using_function(self, function: Callable[[Node], Node]): # noqa: D102 + self.set_operands([function(operand) for operand in self.operands()]) + # TODO: These caches should only be cleared if this is an ArithmeticNode + self._multiplications = None + self._squarings = None + self._depth_cache = None + + + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> FieldArray: # noqa: D102 + # TODO: Remove modulus in this method and store it in each node instead. Alternatively, add `modulus` to methods such as `flatten` as well. + if self._evaluate_cache is None: + self._evaluate_cache = self.operation( + [operand.evaluate(actual_inputs) for operand in self.operands()] + ) + + return self._evaluate_cache + + @abstractmethod + def operation(self, operands: List[FieldArray]) -> FieldArray: + """Evaluates this node on the specified operands.""" + + def arithmetize(self, strategy: str) -> "Node": # noqa: D102 + if self._arithmetize_cache is None: + if self._arithmetize_depth_cache is not None: + return self._arithmetize_depth_cache.get_lowest_value() # type: ignore + + # If we know all operands we can simply evaluate this node + operands = self.operands() + if len(operands) > 0 and all( + hasattr(operand, "_value") for operand in operands + ): # This is a hacky way of checking whether the operands are all constant + from oraqle.compiler.nodes.leafs import Constant + + self._arithmetize_cache = Constant(self.operation([operand._value for operand in self.operands()])) # type: ignore + else: + self._arithmetize_cache = self._arithmetize_inner(strategy) + + return self._arithmetize_cache + + @abstractmethod + def _arithmetize_inner(self, strategy: str) -> "Node": + pass + + # TODO: Reduce code duplication + + def arithmetize_depth_aware(self, cost_of_squaring: float) -> CostParetoFront: # noqa: D102 + if self._arithmetize_depth_cache is None: + if self._arithmetize_cache is not None: + raise Exception("This should not happen") + + # If we know all operands we can simply evaluate this node + operands = self.operands() + if len(operands) > 0 and all( + hasattr(operand, "_value") for operand in operands + ): # This is a hacky way of checking whether the operands are all constant + from oraqle.compiler.nodes.leafs import Constant + + self._arithmetize_depth_cache = CostParetoFront.from_leaf(Constant(self.operation([operand._value for operand in self.operands()])), cost_of_squaring) # type: ignore + else: + self._arithmetize_depth_cache = self._arithmetize_depth_aware_inner( + cost_of_squaring + ) + + assert self._arithmetize_depth_cache is not None + return self._arithmetize_depth_cache + + @abstractmethod + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + pass + + +class BinaryNode(FixedNode): + """A node with two operands.""" diff --git a/oraqle/compiler/nodes/flexible.py b/oraqle/compiler/nodes/flexible.py new file mode 100644 index 0000000..b5cb0be --- /dev/null +++ b/oraqle/compiler/nodes/flexible.py @@ -0,0 +1,164 @@ +"""Module containing nodes with a flexible number of operands.""" +from abc import abstractmethod +from collections import Counter +from functools import reduce +from typing import Callable +from typing import Counter as CounterType +from typing import Dict, Optional, Set, Type + +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.nodes.abstract import CostParetoFront, Node, UnoverloadedWrapper +from oraqle.compiler.nodes.leafs import Constant + + +class FlexibleNode(Node): + """A node with an arbitrary number of operands. The operation must be reducible using a binary associative operation.""" + + # TODO: Ensure that when all inputs are constants, the node is replaced with its evaluation + + def arithmetize(self, strategy: str) -> Node: # noqa: D102 + if self._arithmetize_cache is None: + self._arithmetize_cache = self._arithmetize_inner(strategy) + + return self._arithmetize_cache + + @abstractmethod + def _arithmetize_inner(self, strategy: str) -> "Node": + pass + + def arithmetize_depth_aware(self, cost_of_squaring: float) -> CostParetoFront: # noqa: D102 + if self._arithmetize_depth_cache is None: + self._arithmetize_depth_cache = self._arithmetize_depth_aware_inner(cost_of_squaring) + + return self._arithmetize_depth_cache + + @abstractmethod + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + pass + + +class CommutativeUniqueReducibleNode(FlexibleNode): + """A node with an operation that is reducible without taking order into account: i.e. it has a binary operation that is associative and commutative. + + The operands are unique, i.e. the same operand will never appear twice. + """ + + def __init__( + self, + operands: Set[UnoverloadedWrapper], + gf: Type[FieldArray], + ): + """Initialize a node with the given set as the operands. None of the operands can be a constant.""" + self._operands = operands + assert not any(isinstance(operand.node, Constant) for operand in self._operands) + assert len(operands) > 1 + super().__init__(gf) + + def apply_function_to_operands(self, function: Callable[[Node], None]): # noqa: D102 + for operand in self._operands: + function(operand.node) + + def replace_operands_using_function(self, function: Callable[[Node], Node]): # noqa: D102 + self._operands = {UnoverloadedWrapper(function(operand.node)) for operand in self._operands} + + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> FieldArray: # noqa: D102 + if self._evaluate_cache is None: + self._evaluate_cache = reduce( + self._inner_operation, + (operand.node.evaluate(actual_inputs) for operand in self._operands), + ) + + return self._evaluate_cache + + @abstractmethod + def _inner_operation(self, a: FieldArray, b: FieldArray) -> FieldArray: + """Perform the reducible operation performed by this node (order should not matter).""" + + def __hash__(self) -> int: + if self._hash is None: + # The hash is commutative + hashes = sorted([hash(operand) for operand in self._operands]) + self._hash = hash((self._hash_name, tuple(hashes))) + + return self._hash + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + if hash(self) != hash(other): + return False + + return self._operands == other._operands + + +class CommutativeMultiplicityReducibleNode(FlexibleNode): + """A node with an operation that is reducible without taking order into account: i.e. it has a binary operation that is associative and commutative.""" + + def __init__( + self, + operands: CounterType[UnoverloadedWrapper], + gf: Type[FieldArray], + constant: Optional[FieldArray] = None, + ): + """Initialize a reducible node with the given `Counter` representing the operands, none of which is allowed to be a constant.""" + super().__init__(gf) + self._constant = self._identity if constant is None else constant + self._operands = operands + assert not any(isinstance(operand, Constant) for operand in self._operands) + assert (sum(operands.values()) + (self._constant != self._identity)) > 1 + assert isinstance(next(iter(self._operands)), UnoverloadedWrapper) + + @property + @abstractmethod + def _identity(self) -> FieldArray: + pass + + def apply_function_to_operands(self, function: Callable[[Node], None]): # noqa: D102 + for operand in self._operands: + function(operand.node) + + def replace_operands_using_function(self, function: Callable[[Node], Node]): # noqa: D102 + # FIXME: What if there is only one operand remaining? + self._operands = Counter( + { + UnoverloadedWrapper(function(operand.node)): count + for operand, count in self._operands.items() + } + ) + assert not any(isinstance(operand.node, Constant) for operand in self._operands) + assert (sum(self._operands.values()) + (self._constant != self._identity)) > 1 + + def __hash__(self) -> int: + if self._hash is None: + # The hash is commutative + hashes = sorted( + [(hash(operand.node), count) for operand, count in self._operands.items()] + ) + self._hash = hash((self._hash_name, tuple(hashes), int(self._constant))) + + return self._hash + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + if hash(self) != hash(other): + return False + + return self._operands == other._operands and self._constant == other._constant + + def to_graph(self, graph_builder: DotFile) -> int: # noqa: D102 + if self._to_graph_cache is None: + super().to_graph(graph_builder) + self._to_graph_cache: int + + if self._constant != self._identity: + # TODO: Add known_by + graph_builder.add_link( + graph_builder.add_node(label=str(self._constant)), self._to_graph_cache + ) + + return self._to_graph_cache diff --git a/oraqle/compiler/nodes/leafs.py b/oraqle/compiler/nodes/leafs.py new file mode 100644 index 0000000..35a6725 --- /dev/null +++ b/oraqle/compiler/nodes/leafs.py @@ -0,0 +1,192 @@ +"""Module containing leaf nodes: i.e. nodes without an input.""" +from typing import Any, Dict, List, Set, Tuple, Type + +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.instructions import ArithmeticInstruction, InputInstruction +from oraqle.compiler.nodes.abstract import ArithmeticNode, CostParetoFront, Node, select_stack_index +from oraqle.compiler.nodes.fixed import FixedNode + + +class ArithmeticLeafNode(FixedNode, ArithmeticNode): + """An ArithmeticLeafNode is an ArithmeticNode with no inputs.""" + + def operands(self) -> List[Node]: # noqa: D102 + return [] + + def set_operands(self, operands: List["Node"]): # noqa: D102 + pass + + def _arithmetize_inner(self, strategy: str) -> Node: + return self + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return CostParetoFront.from_leaf(self, cost_of_squaring) + + def multiplicative_depth(self) -> int: # noqa: D102 + return 0 + + def multiplicative_size(self) -> int: # noqa: D102 + return 0 + + def multiplications(self) -> Set[int]: # noqa: D102 + return set() + + def squarings(self) -> Set[int]: # noqa: D102 + return set() + + +# TODO: Merge ArithmeticInput and Input using multiple inheritance +class Input(ArithmeticLeafNode): + """Represents a named input to the arithmetic circuit.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"shape": "circle", "style": "filled", "fillcolor": "lightsteelblue1"} + + @property + def _hash_name(self) -> str: + return "input" + + @property + def _node_label(self) -> str: + return self._name + + def __init__(self, name: str, gf: Type[FieldArray]) -> None: + """Initialize an input with the given `name`.""" + super().__init__(gf) + self._name = name + + + def operation(self, operands: List[FieldArray]) -> FieldArray: # noqa: D102 + raise Exception() + + + def evaluate(self, actual_inputs: Dict[str, FieldArray]) -> FieldArray: # noqa: D102 + return actual_inputs[self._name] + + + def to_graph(self, graph_builder: DotFile) -> int: # noqa: D102 + if self._to_graph_cache is None: + label = self._name + + self._to_graph_cache = graph_builder.add_node( + label=label, **self._overriden_graphviz_attributes + ) + + return self._to_graph_cache + + def __hash__(self) -> int: + return hash(self._name) + + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + return self._name == other._name + + + def create_instructions( # noqa: D102 + self, + instructions: List[ArithmeticInstruction], + stack_counter: int, + stack_occupied: List[bool], + ) -> Tuple[int, int]: + if self._instruction_cache is None: + self._instruction_cache = select_stack_index(stack_occupied) + instructions.append(InputInstruction(self._instruction_cache, self._name)) + + return self._instruction_cache, stack_counter + + +class Constant(ArithmeticLeafNode): + """Represents a Node with a constant value.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"style": "filled", "fillcolor": "red", "shape": "circle"} + + @property + def _hash_name(self) -> str: + return "constant" + + @property + def _node_label(self) -> str: + return str(self._value) + + def __init__(self, value: FieldArray): + """Initialize a Node with the given `value`.""" + super().__init__(value.__class__) + self._value = value + + + def operation(self, operands: List[FieldArray]) -> FieldArray: # noqa: D102 + return self._value + + + def to_graph(self, graph_builder: DotFile) -> Any: # noqa: D102 + if self._to_graph_cache is None: + label = str(self._value) + + self._to_graph_cache = graph_builder.add_node( + label=label, **self._overriden_graphviz_attributes + ) + + return self._to_graph_cache + + def __hash__(self) -> int: + return hash(int(self._value)) + + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + return self._value == other._value + + + def add(self, other: "Node", flatten=True) -> "Node": # noqa: D102 + if isinstance(other, Constant): + return Constant(self._value + other._value) + + return other.add(self, flatten) + + + def mul(self, other: "Node", flatten=True) -> "Node": # noqa: D102 + if isinstance(other, Constant): + return Constant(self._value * other._value) + + return other.mul(self, flatten) + + + def bool_or(self, other: "Node", flatten=True) -> Node: # noqa: D102 + if isinstance(other, Constant): + return Constant(self._gf(bool(self._value) | bool(other._value))) + + return other.bool_or(self, flatten) + + def bool_and(self, other: "Node", flatten=True) -> Node: # noqa: D102 + if isinstance(other, Constant): + return Constant(self._gf(bool(self._value) & bool(other._value))) + + return other.bool_and(self, flatten) + + def create_instructions( # noqa: D102 + self, + instructions: List[ArithmeticInstruction], + stack_counter: int, + stack_occupied: List[bool], + ) -> Tuple[int]: + raise NotImplementedError("The circuit is a constant.") + + +class DummyNode(FixedNode): + """A DummyNode is a fixed node with no inputs and no behavior.""" + + def operands(self) -> List[Node]: # noqa: D102 + return [] + + def set_operands(self, operands: List["Node"]): # noqa: D102 + pass diff --git a/oraqle/compiler/nodes/non_commutative.py b/oraqle/compiler/nodes/non_commutative.py new file mode 100644 index 0000000..4a069b8 --- /dev/null +++ b/oraqle/compiler/nodes/non_commutative.py @@ -0,0 +1,69 @@ +"""A collection of abstract nodes representing operations that are non-commutative.""" +from abc import abstractmethod +from typing import List, Type + +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.nodes.abstract import Node +from oraqle.compiler.nodes.fixed import BinaryNode + + +class NonCommutativeBinaryNode(BinaryNode): + """Represents a non-cummutative binary operation such as `x < y` or `x - y`.""" + + def __init__(self, left, right, gf: Type[FieldArray]): + """Initialize a Node that performs an operation between two operands that is not commutative.""" + self._left = left + self._right = right + super().__init__(gf) + + @abstractmethod + def _operation_inner(self, x, y) -> FieldArray: + """Applies the binary operation on x and y.""" + + def operation(self, operands: List[FieldArray]) -> FieldArray: # noqa: D102 + return self._operation_inner(operands[0], operands[1]) + + def operands(self) -> List[Node]: # noqa: D102 + return [self._left, self._right] + + def set_operands(self, operands: List["Node"]): # noqa: D102 + self._left = operands[0] + self._right = operands[1] + + def __hash__(self) -> int: + if self._hash is None: + left_hash = hash(self._left) + right_hash = hash(self._right) + + self._hash = hash((self._hash_name, (left_hash, right_hash))) + + return self._hash + + def is_equivalent(self, other: Node) -> bool: # noqa: D102 + if not isinstance(other, self.__class__): + return False + + if hash(self) != hash(other): + return False + + return self._left.is_equivalent(other._left) and self._right.is_equivalent(other._right) + + def to_graph(self, graph_builder: DotFile) -> int: # noqa: D102 + if self._to_graph_cache is None: + attributes = {"shape": "box"} + attributes.update(self._overriden_graphviz_attributes) + + self._to_graph_cache = graph_builder.add_node( + label=self._node_label, + **attributes, + ) + + left = self._left.to_graph(graph_builder) + right = self._right.to_graph(graph_builder) + + graph_builder.add_link(left, self._to_graph_cache, headport="nw") + graph_builder.add_link(right, self._to_graph_cache, headport="ne") + + return self._to_graph_cache diff --git a/oraqle/compiler/nodes/unary_arithmetic.py b/oraqle/compiler/nodes/unary_arithmetic.py new file mode 100644 index 0000000..5e74d64 --- /dev/null +++ b/oraqle/compiler/nodes/unary_arithmetic.py @@ -0,0 +1,217 @@ +"""This module contains `ArithmeticNode`s with a single input: Constant additions and constant multiplications.""" +from typing import List, Optional, Set, Tuple + +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.instructions import ( + ArithmeticInstruction, + ConstantAdditionInstruction, + ConstantMultiplicationInstruction, +) +from oraqle.compiler.nodes.abstract import ArithmeticNode, CostParetoFront, Node, select_stack_index +from oraqle.compiler.nodes.univariate import UnivariateNode + +# TODO: There is (going to be) a lot of code duplication between these two classes + + +class ConstantAddition(UnivariateNode, ArithmeticNode): + """This node represents a multiplication of another node with a constant.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"style": "rounded,filled", "fillcolor": "grey80"} + + @property + def _node_shape(self) -> str: + return "square" + + @property + def _hash_name(self) -> str: + return f"constant_add_{self._constant}" + + @property + def _node_label(self) -> str: + return "+" + + def __init__(self, node: ArithmeticNode, constant: FieldArray): + """Represents the operation `constant + node`.""" + super().__init__(node, constant.__class__) + self._constant = constant + assert constant != 0 + + self._depth_cache: Optional[int] = None + + + def _operation_inner(self, input: FieldArray) -> FieldArray: + return input + self._constant + + + def multiplicative_depth(self) -> int: # noqa: D102 + if self._depth_cache is None: + self._depth_cache = self._node.multiplicative_depth() + + return self._depth_cache + + + def multiplications(self) -> Set[int]: # noqa: D102 + return self._node.multiplications() + + + def squarings(self) -> Set[int]: # noqa: D102 + return self._node.squarings() + + + def create_instructions( # noqa: D102 + self, + instructions: List[ArithmeticInstruction], + stack_counter: int, + stack_occupied: List[bool], + ) -> Tuple[int, int]: + self._node: ArithmeticNode + + if self._instruction_cache is None: + operand_index, stack_counter = self._node.create_instructions( + instructions, stack_counter, stack_occupied + ) + + self._node._parent_count -= 1 + if self._node._parent_count == 0: + stack_occupied[self._node._instruction_cache] = False # type: ignore + + self._instruction_cache = select_stack_index(stack_occupied) + + instructions.append( + ConstantAdditionInstruction(self._instruction_cache, operand_index, self._constant) + ) + + return self._instruction_cache, stack_counter + + + def _arithmetize_inner(self, strategy: str) -> Node: + return self + + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + front = CostParetoFront(cost_of_squaring) + for _, _, node in self._node.arithmetize_depth_aware(cost_of_squaring): + front.add(ConstantAddition(node, self._constant)) + return front + + + def to_graph(self, graph_builder: DotFile) -> int: # noqa: D102 + if self._to_graph_cache is None: + super().to_graph(graph_builder) + self._to_graph_cache: int + + # TODO: Add known_by + graph_builder.add_link( + graph_builder.add_node( + label=str(self._constant), shape="circle", style="filled", fillcolor="grey92" + ), + self._to_graph_cache, + ) + + return self._to_graph_cache + + +class ConstantMultiplication(UnivariateNode, ArithmeticNode): + """This node represents a multiplication of another node with a constant.""" + + @property + def _overriden_graphviz_attributes(self) -> dict: + return {"style": "rounded,filled", "fillcolor": "grey80"} + + @property + def _node_shape(self) -> str: + return "square" + + @property + def _hash_name(self) -> str: + return f"constant_mul_{self._constant}" + + @property + def _node_label(self) -> str: + return "ร—" # noqa: RUF001 + + def __init__(self, node: Node, constant: FieldArray): + """Represents the operation `constant * node`.""" + super().__init__(node, constant.__class__) + self._constant = constant + assert constant != 0 + assert constant != 1 + + self._depth_cache: Optional[int] = None + + def _operation_inner(self, input: FieldArray) -> FieldArray: + return input * self._constant # type: ignore + + + def multiplicative_depth(self) -> int: # noqa: D102 + if self._depth_cache is None: + self._depth_cache = self._node.multiplicative_depth() # type: ignore + + return self._depth_cache # type: ignore + + + def multiplications(self) -> Set[int]: # noqa: D102 + return self._node.multiplications() # type: ignore + + + def squarings(self) -> Set[int]: # noqa: D102 + return self._node.squarings() # type: ignore + + + def create_instructions( # noqa: D102 + self, + instructions: List[ArithmeticInstruction], + stack_counter: int, + stack_occupied: List[bool], + ) -> Tuple[int, int]: + self._node: ArithmeticNode + + if self._instruction_cache is None: + operand_index, stack_counter = self._node.create_instructions( + instructions, stack_counter, stack_occupied + ) + + self._node._parent_count -= 1 + if self._node._parent_count == 0: + stack_occupied[self._node._instruction_cache] = False # type: ignore + + self._instruction_cache = select_stack_index(stack_occupied) + + instructions.append( + ConstantMultiplicationInstruction( + self._instruction_cache, operand_index, self._constant + ) + ) + + return self._instruction_cache, stack_counter + + + def _arithmetize_inner(self, strategy: str) -> Node: + return self + + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + front = CostParetoFront(cost_of_squaring) + for _, _, node in self._node.arithmetize_depth_aware(cost_of_squaring): + front.add(ConstantMultiplication(node, self._constant)) + return front + + + def to_graph(self, graph_builder: DotFile) -> int: # noqa: D102 + if self._to_graph_cache is None: + super().to_graph(graph_builder) + self._to_graph_cache: int + + # TODO: Add known_by + graph_builder.add_link( + graph_builder.add_node( + label=str(self._constant), shape="circle", style="filled", fillcolor="grey92" + ), + self._to_graph_cache, + ) + + return self._to_graph_cache diff --git a/oraqle/compiler/nodes/univariate.py b/oraqle/compiler/nodes/univariate.py new file mode 100644 index 0000000..35f3a19 --- /dev/null +++ b/oraqle/compiler/nodes/univariate.py @@ -0,0 +1,81 @@ +"""Abstract nodes for univariate operations.""" + +from abc import abstractmethod +from typing import List, Type + +from galois import FieldArray + +from oraqle.compiler.graphviz import DotFile +from oraqle.compiler.nodes.abstract import Node +from oraqle.compiler.nodes.fixed import FixedNode +from oraqle.compiler.nodes.leafs import Constant + + +class UnivariateNode(FixedNode): + """An abstract node with a single input.""" + + @property + @abstractmethod + def _node_shape(self) -> str: + """Graphviz node shape.""" + + def __init__(self, node: Node, gf: Type[FieldArray]): + """Initialize a univariate node.""" + self._node = node + assert not isinstance(node, Constant) + super().__init__(gf) + + + def operands(self) -> List["Node"]: # noqa: D102 + return [self._node] + + + def set_operands(self, operands: List["Node"]): # noqa: D102 + self._node = operands[0] + + @abstractmethod + def _operation_inner(self, input: FieldArray) -> FieldArray: + """Evaluate the operation on the input. This method does not have to cache.""" + + + def operation(self, operands: List[FieldArray]) -> FieldArray: # noqa: D102 + return self._operation_inner(operands[0]) + + + def to_graph(self, graph_builder: DotFile) -> int: # noqa: D102 + if self._to_graph_cache is None: + attributes = {} + + attributes.update(self._overriden_graphviz_attributes) + + self._to_graph_cache = graph_builder.add_node( + label=self._node_label, shape=self._node_shape, **attributes + ) + + graph_builder.add_link(self._node.to_graph(graph_builder), self._to_graph_cache) + + return self._to_graph_cache + + def __hash__(self) -> int: + if self._hash is None: + self._hash = hash((self._hash_name, self._node)) + + return self._hash + + def is_equivalent(self, other: Node) -> bool: + """Check whether `self` is semantically equivalent to `other`. + + This function may have false negatives but it should never return false positives. + + Returns: + ------- + `True` if `self` is semantically equivalent to `other`, `False` if they are not or that they cannot be shown to be equivalent. + + """ + if not isinstance(other, self.__class__): + return False + + if hash(self) != hash(other): + return False + + return self._node.is_equivalent(other._node) diff --git a/oraqle/compiler/poly2circuit.py b/oraqle/compiler/poly2circuit.py new file mode 100644 index 0000000..c481b27 --- /dev/null +++ b/oraqle/compiler/poly2circuit.py @@ -0,0 +1,149 @@ +"""Module for automatic circuit generation for any functions with any number of inputs. + +Warning: These circuits can be very large! +""" + +from collections import Counter +from typing import Dict, List, Tuple, Type + +from galois import GF, FieldArray +from sympy import Add, Integer, Mul, Poly, Pow, Symbol +from sympy.core.numbers import NegativeOne + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.func2poly import interpolate_polynomial +from oraqle.compiler.nodes import Constant, Input, Node +from oraqle.compiler.nodes.abstract import UnoverloadedWrapper +from oraqle.compiler.nodes.arbitrary_arithmetic import Product + + +def construct_subcircuit(expression, gf, modulus: int, inputs: Dict[str, Input]) -> Node: # noqa: PLR0912 + """Build a circuit with a single output given an expression of simple arithmetic operations in Sympy. + + Raises: + ------ + Exception: Exponents must be integers, or an exception will be raised. + + Returns: + ------- + A subcircuit (Node) computing the given sympy expression. + + """ + if expression.func == Add: + arg_iter = iter(expression.args) + + # The first argument can be a scalar. + first = next(arg_iter) + if first.func in {Integer, NegativeOne}: + if first.func == Integer: + scalar = Constant(gf(int(first) % modulus)) + else: + scalar = Constant(-gf(1)) + result = scalar + construct_subcircuit(next(arg_iter), gf, modulus, inputs) + else: + # TODO: Replace this entire part with a sum + result = construct_subcircuit(first, gf, modulus, inputs) + construct_subcircuit( + next(arg_iter), gf, modulus, inputs + ) + + for arg in arg_iter: + result = construct_subcircuit(arg, gf, modulus, inputs) + result + + return result + elif expression.func == Mul: + arg_iter = iter(expression.args) + + # The first argument can be a scalar. + first = next(arg_iter) + if first.func in {Integer, NegativeOne}: + if first.func == Integer: + scalar = Constant(gf(int(first) % modulus)) + else: + scalar = Constant(-gf(1)) + result = scalar * construct_subcircuit(next(arg_iter), gf, modulus, inputs) + else: + # TODO: Replace this entire part with a product + result = construct_subcircuit(first, gf, modulus, inputs) * construct_subcircuit( + next(arg_iter), gf, modulus, inputs + ) + + for arg in arg_iter: + result = construct_subcircuit(arg, gf, modulus, inputs) * result + + return result + elif expression.func == Pow: + if expression.args[1].func != Integer: + raise Exception("There was an exponent with a non-integer exponent") + # Change powers to series of multiplications + subcircuit = construct_subcircuit(expression.args[0], gf, modulus, inputs) + # TODO: This is not the most efficient way; we can use re-balancing. + return Product( + Counter({UnoverloadedWrapper(subcircuit): int(expression.args[1])}), gf + ) # FIXME: This could be flattened + elif expression.func == Symbol: + assert len(expression.args) == 0 + var = str(expression) + if var in inputs: + return inputs[var] + new_input = Input(var, gf) + inputs[var] = new_input + return new_input + else: + raise Exception( + f"The expression contained an invalid operation (not one implemented in arithmetic circuits): {expression.func}." + ) + + +def construct_circuit(polynomials: List[Poly], modulus: int) -> Tuple[Circuit, Type[FieldArray]]: + """Construct an arithmetic circuit from a list of polynomials and the fixed modulus. + + Returns: + ------- + A circuit outputting the evaluation of each polynomial. + + """ + inputs = {} + gf = GF(modulus) + return ( + Circuit( + [construct_subcircuit(poly.expr, gf, modulus, inputs) for poly in polynomials], + ), + gf, + ) + + +if __name__ == "__main__": + # Use function max(x, y) + function = max + modulus = 7 + + # Create a polynomial and then a circuit that evalutes this expression + poly = interpolate_polynomial(function, modulus, ["x", "y"]) + circuit, gf = construct_circuit([poly], modulus) + + # Output a DOT file for this high-level circuit (you can visualize it using https://dreampuf.github.io/GraphvizOnline/) + circuit.to_graph("max_7_hl.dot") + + # Arithmetize the high-level circuit, afterwards it will only contain arithmetic operations + circuit = circuit.arithmetize() + circuit.to_graph("max_7_hl.dot") + + # Print the initial metrics of the circuit + print("depth", circuit.multiplicative_depth()) + print("size", circuit.multiplicative_size()) + + # Apply common subexpression elimination (CSE) to remove duplicate operations from the circuit + circuit.eliminate_subexpressions() + + # Output a DOT file for this arithmetic circuit (you can visualize it using https://dreampuf.github.io/GraphvizOnline/) + circuit.to_graph("max_7.dot") + + # Print the resulting metrics of the circuit + print("depth", circuit.multiplicative_depth()) + print("size", circuit.multiplicative_size()) + + # Test that given x=4 and y=2 indeed max(x, y) = 4 + assert circuit.evaluate({"x": gf(4), "y": gf(2)}) == [4] + + # Output a DOT file for this arithmetic circuit (you can visualize it using https://dreampuf.github.io/GraphvizOnline/) + circuit.to_graph("max_7.dot") diff --git a/oraqle/compiler/polynomials/__init__.py b/oraqle/compiler/polynomials/__init__.py new file mode 100644 index 0000000..1ca2682 --- /dev/null +++ b/oraqle/compiler/polynomials/__init__.py @@ -0,0 +1,5 @@ +"""The polynomials package contains nodes for performing polynomial evaluation. + +In a finite field, the set of polyfunctions is the same as the set of all functions. +So, you can perform any function by interpolating a polynomial. +""" diff --git a/oraqle/compiler/polynomials/univariate.py b/oraqle/compiler/polynomials/univariate.py new file mode 100644 index 0000000..cd306c8 --- /dev/null +++ b/oraqle/compiler/polynomials/univariate.py @@ -0,0 +1,620 @@ +"""Evaluation of univariate polynomials.""" + +import math +from typing import Callable, Dict, List, Optional, Tuple, Type + +from galois import GF, FieldArray + +from oraqle.add_chains.addition_chains_heuristic import add_chain_guaranteed +from oraqle.compiler.arithmetic.subtraction import Subtraction +from oraqle.compiler.func2poly import interpolate_polynomial +from oraqle.compiler.nodes.abstract import ArithmeticNode, CostParetoFront, Node +from oraqle.compiler.nodes.binary_arithmetic import Multiplication +from oraqle.compiler.nodes.leafs import Constant, Input +from oraqle.compiler.nodes.unary_arithmetic import ConstantMultiplication +from oraqle.compiler.nodes.univariate import UnivariateNode +from oraqle.config import PS_METHOD_FACTOR_K + + +def _format_polynomial(coefficients: List[FieldArray]) -> str: + degree = len(coefficients) - 1 + if degree == 0: + return str(coefficients[0]) + + terms = [] + for i, coef in enumerate(coefficients): + if coef == 0: + # Skip zero coefficients + continue + + term = str(coef) if i == 0 or coef > 1 else "" + + if i > 0: + term += "x" + + if i > 1: + term += f"^{i}" + + if term != "": + terms.append(term) + + polynomial = " + ".join(terms) + return polynomial + + +class UnivariatePoly(UnivariateNode): + """Evaluation of a univariate polynomial.""" + + @property + def _node_shape(self) -> str: + return "box" + + @property + def _hash_name(self) -> str: + return "univariate_poly" + + @property + def _node_label(self) -> str: + return _format_polynomial(self._coefficients) + + def __init__( + self, + node: Node, + coefficients: List[FieldArray], + gf: Type[FieldArray], + ): + """Initialize a univariate polynomial with the given coefficients from least to highest order.""" + self._coefficients = coefficients + # TODO: We can reduce this polynomial if its degree is too high + super().__init__(node, gf) + + self._custom_arithmetize_cache = None + + @classmethod + def from_function( + cls, node: Node, gf: Type[FieldArray], function: Callable[[int], int] + ) -> "UnivariatePoly": + """Interpolate a univariate polynomial for the given function. + + Returns: + ------- + A UnivariatePoly whose coefficients compute the `function` on all inputs. + + """ + coefficients = [ + gf(int(coeff) % gf.characteristic) + for coeff in reversed( + interpolate_polynomial(function, gf.characteristic, ["x"]).as_list() + ) + ] + return cls(node, coefficients, gf) + + def _operation_inner(self, input: FieldArray) -> FieldArray: + coefficient_iter = iter(self._coefficients) + result = next(coefficient_iter).copy() + + x_pow = input.copy() + for coefficient in coefficient_iter: + result += coefficient * x_pow + x_pow *= input + + return result # type: ignore + + def _arithmetize_inner(self, strategy: str) -> Node: + return self.arithmetize_custom(strategy)[0] + + def arithmetize_custom(self, strategy: str) -> Tuple[ArithmeticNode, Dict[int, ArithmeticNode]]: + """Compute an arithmetization along with a dictionary of precomputed powers. + + Returns: + ------- + An arithmetization and a dictionary of previously computed powers. + + """ + if len(self._coefficients) == 0: + return Constant(self._gf(0)), {} + + if len(self._coefficients) == 1: + return Constant(self._coefficients[0]), {} + + x = self._node.arithmetize(strategy).to_arithmetic() + + best_arithmetization: Optional[Node] = None + best_arithmetization_powers = None + + lowest_multiplicative_size = 1_000_000_000 # TODO: Not elegant + optimal_k = math.sqrt(2 * len(self._coefficients)) + bound = min(int(math.ceil(PS_METHOD_FACTOR_K * optimal_k)), len(self._coefficients)) + for k in range(1, bound): + ( + arithmetization, + precomputed_powers, + ) = _eval_poly(x, self._coefficients, k, self._gf, 1.0) + + arithmetization = arithmetization.to_arithmetic() + # TODO: It would be best to perform CSE during the circuit creation + assert isinstance(arithmetization, ArithmeticNode) + + if arithmetization.multiplicative_size() <= lowest_multiplicative_size: + lowest_multiplicative_size = arithmetization.multiplicative_size() + best_arithmetization = arithmetization + best_arithmetization_powers = precomputed_powers + + # TODO: Also perform the alternative poly evaluation + + # TODO: This check is probably unnecessary + assert best_arithmetization is not None + assert best_arithmetization_powers is not None + + return ( + best_arithmetization.arithmetize(strategy), + best_arithmetization_powers, + ) + + def _arithmetize_depth_aware_inner(self, cost_of_squaring: float) -> CostParetoFront: + return self.arithmetize_depth_aware_custom(cost_of_squaring)[0] + + def arithmetize_depth_aware_custom( + self, cost_of_squaring: float + ) -> Tuple[CostParetoFront, Dict[int, Dict[int, ArithmeticNode]]]: + """Compute a depth-aware arithmetization as well as a dictionary indexed by the depth of the nodes in the front. The dictionary stores precomputed powers. + + Returns: + ------- + A CostParetoFront with the depth-aware arithmetization and a dictionary indexed by the depth of the nodes in the front, returning a dictionary with previously computed powers. + + """ + # TODO: Perhaps this should be cached + if len(self._coefficients) == 0: + return CostParetoFront.from_leaf(Constant(self._gf(0)), cost_of_squaring), {0: {}} + + if len(self._coefficients) == 1: + return CostParetoFront.from_leaf(Constant(self._coefficients[0]), cost_of_squaring), { + 0: {} + } + + front = CostParetoFront(cost_of_squaring) + all_precomputed_powers = {} + + for _, _, x in self._node.arithmetize_depth_aware(cost_of_squaring): + optimal_k = math.sqrt(2 * len(self._coefficients)) + bound = min(int(math.ceil(PS_METHOD_FACTOR_K * optimal_k)), len(self._coefficients)) + for k in range(1, bound): + ( + arithmetization, + precomputed_powers, + ) = _eval_poly(x, self._coefficients, k, self._gf, cost_of_squaring) + + arithmetization = arithmetization.to_arithmetic() + assert isinstance(arithmetization, ArithmeticNode) + + added = front.add(arithmetization) + if added: + all_precomputed_powers[arithmetization.multiplicative_depth()] = ( + precomputed_powers + ) + + for k in range(1, len(self._coefficients)): + ( + arithmetization, + precomputed_powers, + ) = _eval_poly_divide_conquer(x, self._coefficients, k, self._gf, cost_of_squaring) + + arithmetization = arithmetization.to_arithmetic() + assert isinstance(arithmetization, ArithmeticNode) + + added = front.add(arithmetization) + if added: + all_precomputed_powers[arithmetization.multiplicative_depth()] = ( + precomputed_powers + ) + + for k in range(1, len(self._coefficients)): + ( + arithmetization, + precomputed_powers, + ) = _eval_poly_alternative(x, self._coefficients, k, self._gf) + + arithmetization = arithmetization.to_arithmetic() + assert isinstance(arithmetization, ArithmeticNode) + + added = front.add(arithmetization) + if added: + all_precomputed_powers[arithmetization.multiplicative_depth()] = ( + precomputed_powers + ) + + precomputed_powers = {depth: all_precomputed_powers[depth] for depth, _, _ in front} + return front, precomputed_powers + + +def _monic_euclidean_division( + a: List[FieldArray], b: List[FieldArray], gf +) -> Tuple[List[FieldArray], List[FieldArray]]: + q = [gf(0) for _ in range(len(a))] + r = [el.copy() for el in a] + d = len(b) - 1 + c = b[-1].copy() + assert c == 1 + while (len(r) - 1) >= d: + if r[-1] == 0: + r.pop() + continue + + s_monomial = len(r) - 1 - d + f = r[-1] + q[s_monomial] += f + + for i in range(d + 1): + r[s_monomial + i] -= f * b[i] + r.pop() + + while len(q) > 0 and q[-1] == 0: + q.pop() + + return q, r + + +def _eval_poly_using_precomputed_ks( + coefficients: List[FieldArray], precomputed_ks: List[ArithmeticNode], gf +) -> ArithmeticNode: + if len(coefficients) == 0: + return Constant(gf(0)) + + # TODO: What if the constant is 0? Do we want to rely on no-op removal later or do it here already? + output = Constant(coefficients[0]) + + for i in range(1, len(coefficients)): + if coefficients[i] == 0: + continue + + if coefficients[i] == 1: + output += precomputed_ks[i - 1] + continue + + output += ( + Constant(coefficients[i]).mul(precomputed_ks[i - 1], flatten=False) + ) # FIXME: Consider just using * + + return output.arithmetize("best-effort").to_arithmetic() + + +def _eval_monic_poly_specific( + coefficients: List[FieldArray], + precomputed_ks: List[ArithmeticNode], + precomputed_pow2s: List[ArithmeticNode], + gf, + p: int, +) -> ArithmeticNode: + if all(c == 0 for c in coefficients): + return Constant(gf(0)) + + degree = len(coefficients) - 1 + + # Base case, this is free after precomputation + if degree <= len(precomputed_ks): + return _eval_poly_using_precomputed_ks(coefficients, precomputed_ks, gf) + + assert degree % len(precomputed_ks) == 0 + assert ((degree // len(precomputed_ks)) + 1) % 2 == 0 + + k = len(precomputed_ks) + assert p == (((degree // k) + 1) // 2) + + r = coefficients[: (k * p - 1) + 1] + q = coefficients[(k * p - 1) + 1 :] + + assert (len(q) - 1) == k * (p - 1) + + r[k * (p - 1)] = r[k * (p - 1)].copy() - gf(1) + c, s = _monic_euclidean_division(r, q, gf) + assert len(c) - 1 <= (len(precomputed_ks) - 1) + + monomial = precomputed_pow2s[int(math.log2(p))] + + c_output = _eval_poly_using_precomputed_ks(c, precomputed_ks, gf) + + left = monomial.add(c_output, flatten=False) + right = _eval_monic_poly_specific(q, precomputed_ks, precomputed_pow2s, gf, p // 2) + + s.append(gf(1)) # This adds the monomial + assert (len(s) - 1) == k * (p - 1) + remainder = _eval_monic_poly_specific(s, precomputed_ks, precomputed_pow2s, gf, p // 2) + + final_product = left.mul(right, flatten=False) + return ( + final_product.add(remainder, flatten=False).arithmetize("best-effort").to_arithmetic() + ) # TODO: Strategy + + +def _precompute_ks(x: ArithmeticNode, k: int) -> List[ArithmeticNode]: + # TODO: We can use an addition sequence for this to reduce the multiplicative cost + ks = [x] + for _ in range(math.ceil(math.log2(k))): + last = ks[-1] + new_ks = [] + for pre in ks: + new_ks.append(Multiplication(pre, last, pre._gf)) + ks.extend(new_ks) + + return ks[:k] + + +def _compute_extended_monomial( + x: ArithmeticNode, + precomputed_powers: Dict[int, ArithmeticNode], + target: int, + gf: Type[FieldArray], + squaring_cost: float, +) -> ArithmeticNode: + if target == 0: + return Constant(gf(1)) + + # TODO: Use squaring_cost + p = gf.characteristic + precomputed_values = tuple( + ( + exp % (p - 1), + power_node.multiplicative_depth() - x.multiplicative_depth(), + ) + for exp, power_node in precomputed_powers.items() + ) + # TODO: This is copied from Power, but in the future we can probably remove this if we have augmented circuits + addition_chain = add_chain_guaranteed(target, modulus=p - 1, squaring_cost=squaring_cost, precomputed_values=precomputed_values) + + nodes = [x] + nodes.extend(power_node for _, power_node in precomputed_powers.items()) + + for i, j in addition_chain: + nodes.append(Multiplication(nodes[i], nodes[j], gf)) + + return nodes[-1] + + +def _eval_poly( + x: ArithmeticNode, + coefficients: List[FieldArray], + k: int, + gf: Type[FieldArray], + squaring_cost: float, +) -> Tuple[ArithmeticNode, Dict[int, ArithmeticNode]]: + # Paterson & Stockmeyer's algorithm + degree = len(coefficients) - 1 + precomputed_ks = _precompute_ks(x, k) + precomputed_powers = { + i % (gf.characteristic - 1): node for i, node in zip(range(1, k + 1), precomputed_ks) + } + + # Find the largest p such that k(2^p-1) >= degree + p = 0 + while True: + p += 1 + if (2**p - 1) * k >= degree: + break + + new_degree = (2**p - 1) * k + precomputed_pow2s = [precomputed_ks[-1]] + for j in range(p - 1): # TODO: Check if p - 1 is enough + precomputed_pow2s.append( + Multiplication(precomputed_pow2s[-1], precomputed_pow2s[-1], precomputed_pow2s[-1]._gf) + ) + precomputed_powers[(k * (2 ** (j + 1))) % (gf.characteristic - 1)] = precomputed_pow2s[-1] + + # Pad to the next degree k * (2^p - 1) monic polynomial + new_coefficients = [gf(0) for _ in range(new_degree + 1)] + for j, c in enumerate(coefficients): + new_coefficients[j] = c.copy() + + extended = new_coefficients[-1] == 0 + factor = gf(1) + if int(new_coefficients[-1]) > 1: + # The polynomial is not monic + inverse = coefficients[-1] ** -1 + new_coefficients = [inverse * c for c in coefficients] + factor = coefficients[-1] + + new_coefficients[-1] = gf(1) + + monomial_index = new_degree % (gf.characteristic - 1) + if monomial_index == 0: + monomial_index = gf.characteristic - 1 + if extended and monomial_index <= degree: + # In some cases we can eliminate the added monomial by changing the coefficients + new_coefficients[monomial_index] -= gf(1) + extended = False + + evaluation = _eval_monic_poly_specific( + new_coefficients, precomputed_ks, precomputed_pow2s, gf, 2**p // 2 + ) + + if extended: + monomial = _compute_extended_monomial( + x, precomputed_powers, new_degree % (gf.characteristic - 1), gf, squaring_cost + ) + precomputed_powers[new_degree % (gf.characteristic - 1)] = monomial + evaluation = ( + Subtraction(evaluation, monomial, gf).arithmetize("best-effort").to_arithmetic() + ) # TODO: We should not have to choose a strategy here + + if int(factor) > 1: + # Make up for the missing factor + evaluation = ConstantMultiplication(evaluation, factor) + + return evaluation, precomputed_powers + + +def _eval_poly_alternative( + x: ArithmeticNode, coefficients: List[FieldArray], k: int, gf: Type[FieldArray] +) -> Tuple[Node, Dict[int, ArithmeticNode]]: + # Baby-step giant-step algorithm + assert len(coefficients) > 0 + + i = len(coefficients) - 1 + while coefficients[i] == 0: + i -= 1 + coefficients = [coefficients[j].copy() for j in range(i + 1)] # Copies and trims the coefficients + + # Precompute x, x^2, ..., x^k + precomputed_ks = _precompute_ks(x, k) + precomputed_powers = { + i % (gf.characteristic - 1): node for i, node in zip(range(1, k + 1), precomputed_ks) + } + + # Process the first chunk + chunk = coefficients[-(k + 1) :] + aggregator = _eval_poly_using_precomputed_ks(chunk, precomputed_ks, gf) + coefficients = coefficients[: -(k + 1)] + + # Go through the coefficients, chunk by chunk + while len(coefficients) >= k: + chunk = coefficients[-k:] + aggregator = aggregator * precomputed_ks[-1] + _eval_poly_using_precomputed_ks( + chunk, precomputed_ks, gf + ) + coefficients = coefficients[:-k] + + # If there is a small chunk remaining + if len(coefficients) > 0: + aggregator = aggregator * precomputed_ks[ + len(coefficients) - 1 + ] + _eval_poly_using_precomputed_ks(coefficients, precomputed_ks, gf) + + return aggregator, precomputed_powers + + +def _eval_poly_divide_conquer_specific( + coefficients: List[FieldArray], + precomputed_ks: List[ArithmeticNode], + precomputed_pow2s: List[ArithmeticNode], + gf, + p: int, +) -> ArithmeticNode: + if all(c == 0 for c in coefficients): + return Constant(gf(0)) + + degree = len(coefficients) - 1 + + # Base case, this is free after precomputation + if degree <= len(precomputed_ks): + return _eval_poly_using_precomputed_ks(coefficients, precomputed_ks, gf) + + assert degree / 2 <= (len(precomputed_ks) * p) + + subdegree = p * len(precomputed_ks) + r = coefficients[:subdegree] + q = coefficients[subdegree:] + + r_eval = _eval_poly_divide_conquer_specific(r, precomputed_ks, precomputed_pow2s, gf, p // 2) + q_eval = _eval_poly_divide_conquer_specific(q, precomputed_ks, precomputed_pow2s, gf, p // 2) + + final_product = q_eval.mul(precomputed_pow2s[int(math.log2(p))], flatten=False) + return ( + final_product.add(r_eval, flatten=False).arithmetize("best-effort").to_arithmetic() + ) # TODO: Strategy + + +def _eval_poly_divide_conquer( + x: ArithmeticNode, + coefficients: List[FieldArray], + k: int, + gf: Type[FieldArray], + _squaring_cost: float, +) -> Tuple[ArithmeticNode, Dict[int, ArithmeticNode]]: + # Divide-and-conquer algorithm + # TODO: Reduce code duplication with poly_eval + degree = len(coefficients) - 1 + precomputed_ks = _precompute_ks(x, k) + precomputed_powers = { + i % (gf.characteristic - 1): node for i, node in zip(range(1, k + 1), precomputed_ks) + } + + # Find the largest p such that k * 2^p >= degree + p = 0 + while True: + p += 1 + if 2**p * k >= degree: + break + + precomputed_pow2s = [precomputed_ks[-1]] + for j in range(p - 1): # TODO: Check if p - 1 is enough + precomputed_pow2s.append( + Multiplication(precomputed_pow2s[-1], precomputed_pow2s[-1], precomputed_pow2s[-1]._gf) + ) + precomputed_powers[(k * (2 ** (j + 1))) % (gf.characteristic - 1)] = precomputed_pow2s[-1] + + evaluation = _eval_poly_divide_conquer_specific( + coefficients, precomputed_ks, precomputed_pow2s, gf, 2 ** (p - 1) + ) + + return evaluation, precomputed_powers + + +def _eval_coefficients(x: FieldArray, coefficients: List[FieldArray]) -> FieldArray: + x_pow = x.copy() + result = coefficients[0].copy() + + for coeff in coefficients[1:]: + result += x_pow * coeff + x_pow *= x + + return result + + +def test_ps_method(): # noqa: D103 + gf = GF(31) + coefficients = [gf(i) for i in range(31)] + + x = Input("x", gf) + + for k in range(1, len(coefficients)): + ( + arithmetization, + _, + ) = _eval_poly(x, coefficients, k, gf, squaring_cost=1.0) + arithmetization.clear_cache(set()) + + for xx in range(31): + assert arithmetization.evaluate({"x": gf(xx)}) == _eval_coefficients(gf(xx), coefficients) + arithmetization.clear_cache(set()) + + assert all(coefficients[i] == i for i in range(31)) + + +def test_divide_conquer_method(): # noqa: D103 + gf = GF(31) + coefficients = [gf(i) for i in range(31)] + + x = Input("x", gf) + + for k in range(1, len(coefficients)): + ( + arithmetization, + _, + ) = _eval_poly_divide_conquer(x, coefficients, k, gf, _squaring_cost=1.0) + arithmetization.clear_cache(set()) + + for xx in range(31): + assert arithmetization.evaluate({"x": gf(xx)}) == _eval_coefficients(gf(xx), coefficients) + arithmetization.clear_cache(set()) + + assert all(coefficients[i] == i for i in range(31)) + + +def test_babystep_giantstep_method(): # noqa: D103 + gf = GF(31) + coefficients = [gf(i) for i in range(31)] + + x = Input("x", gf) + + for k in range(1, len(coefficients)): + ( + arithmetization, + _, + ) = _eval_poly_alternative(x, coefficients, k, gf) + arithmetization.clear_cache(set()) + + for xx in range(31): + assert arithmetization.evaluate({"x": gf(xx)}) == _eval_coefficients(gf(xx), coefficients) + arithmetization.clear_cache(set()) + + assert all(coefficients[i] == i for i in range(31)) diff --git a/oraqle/config.py b/oraqle/config.py new file mode 100644 index 0000000..e47979c --- /dev/null +++ b/oraqle/config.py @@ -0,0 +1,27 @@ +"""This module contains global configuration options. + +!!! warning + This is almost certainly going to be removed in the future. +We do not want oraqle to have a global configuration, but this is currently an intentional evil to prevent large refactors in the initial versions. +""" +from typing import Annotated, Optional + + +Seconds = Annotated[float, "seconds"] +MAXSAT_TIMEOUT: Optional[Seconds] = None +"""Time-out for individual calls to the MaxSAT solver. + +!!! danger + This causes non-deterministic behavior! + +!!! bug + There is currently a chance to get `AttributeError`s, which is a bug caused by pysat trying to delete an oracle that does not exist. + There is no current workaround for this.""" + + +PS_METHOD_FACTOR_K: float = 2.0 +"""Approximation factor for the PS-method, higher is better. + +The Paterson-Stockmeyer method takes a value k, that is theoretically optimal when k = sqrt(2 * degree). +However, sometimes it is better to try other values of k (e.g. due to rounding and to trade off depth and cost). +This factor, let's call it f, is used to limit the candidate values of k that we try: [1, f * sqrt(2 * degree)).""" diff --git a/oraqle/demo/depth_aware_equality.ipynb b/oraqle/demo/depth_aware_equality.ipynb new file mode 100644 index 0000000..9a7a6f6 --- /dev/null +++ b/oraqle/demo/depth_aware_equality.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3580d661-a471-4131-a3e3-a88161d38209", + "metadata": {}, + "source": [ + "# A new paradigm in arithmetization: Depth-aware arithmetization" + ] + }, + { + "cell_type": "markdown", + "id": "d97c8705-c1d1-4b3b-848e-0a68dc7a703b", + "metadata": {}, + "source": [ + "#### An equality circuit is easy to define but somewhat hard to optimize!" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b615a377-1777-4aec-acb6-79f202dec6ac", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from galois import GF\n", + "\n", + "from circuit_compiler.compiler.nodes.leafs import Input\n", + "from circuit_compiler.compiler.comparison.equality import Equals\n", + "from circuit_compiler.compiler.circuit import Circuit\n", + "\n", + "gf = GF(467)\n", + "\n", + "a = Input(\"a\", gf)\n", + "b = Input(\"b\", gf)\n", + "\n", + "output = Equals(a, b, gf)\n", + "\n", + "circuit = Circuit(outputs=[output], gf=gf)\n", + "circuit.display_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "e466a3dd-b3ef-4449-979b-c443bf860f79", + "metadata": {}, + "source": [ + "#### Naive methods only find only one arithmetization for this high-level circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "3828eb5d-6da2-421f-b5e0-9cc0e399d434", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAO0AAAWFCAYAAADl0lAfAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nOzdZ1RUV//34c/AANIsKBorBqOgsaLGLjbEHmJBxN57vDVqYoy9xZLExFgRsSOWoFEssZDYjbFhA2NDjYIoSAdhZp4X/OGJERGYM5yZYV9rZa0Iw96/Sfyyz5yzi0Kj0WgQBMFgmMhdgCAIeSNCKwgGRoRWEAyMUu4CBMMQFxfHw4cPSUhIIDExkVevXmFlZYWNjQ02NjaULVuWcuXKyV1moSBCK7wlLi6OkydPcuLECa5evUpYWBhPnz5978/Z2tpSrVo1atasiaurK61bt6Zy5cq6L7iQUYi7xwJATEwMO3fuZOvWrZw7dw61Wk2NGjWoX78+VapUoWrVqlSuXBlra2usrKwoXrw4SUlJJCYmkpSUxD///MO9e/e4e/cuN27c4OLFiyQnJ+Po6IiXlxf9+/fH2dlZ7rdpFERoC7lr166xePFiAgMDUSgUdOrUic6dO9OsWTPs7Ozy3e7r16/566+/OH78OLt37+bp06c0atSISZMm0bNnT0xMxO2U/BKhLaT++usv5syZQ1BQEDVr1mTYsGF07doVW1tbyftSq9WcPn2azZs3s3//fqpWrcrXX39Nv379RHjzQYS2kImOjubrr7/Gx8eH+vXrM3HiRNq1a4dCoSiQ/u/du8ePP/7I7t27cXFxYdWqVbi4uBRI38ZC/JorRHbv3o2zszN79+5l9erVHDx4EDc3twILLECVKlX46aefOH78OAqFgk8++YRJkyaRmppaYDUYOjHSFgIpKSl88cUXrFq1ioEDBzJz5kyKFi0qd1loNBoCAgL4+uuvcXJyYseOHVSpUkXusvSeCK2Re/bsGV26dOHvv/9m+fLldOvWTe6S3nL//n2GDx9OeHg4AQEBuLu7y12SXisUoU1OTubOnTtERUURHx9Peno6VlZWWFtb4+DggIODg1HeELl79y7u7u6YmJiwfft2PvzwQ7lLeqfU1FS++OILfvnlF/z8/Ojbt6/cJekto5xc8fz5c4KCgggODubUqdOEhz8kp99NFhYW1KxZi1atXGnXrh1t27bFzMysACuW3vXr13Fzc6NcuXL4+/tTsmRJuUvKkYWFBStWrMDe3p7+/fsTExPDuHHj5C5LLxnNSKvRaDhw4ABr163jtyNHMDVVUrtBU+o1cqWKU00cHJ0oVbosltY2KJVmJCclkpKcyD+PHvDofhi3r1/i0rkT3Au7iZ1dSby9+zBu3DiqVasm91vLs4cPH9KsWTMcHBzw9/fH2tpa7pLyZPny5SxcuJAtW7aIETcbRhHa3bt3M2fOXG7evEHjlu3p1GMArdw/w6KIZZ7binj6iEO/bCVo90aehN+jZ8+ezJ07FycnJx1ULr2oqCiaN2+OhYUFe/fu1YsbTvkxe/Zs1q1bx6+//kqHDh3kLkevGHRo//77b8aMGcvx48do382LgWOm8ZFzLUnaVqvVBB/aw4af5hF+P4wpU6Ywffp0LC3z/ougoKjVatq3b8/du3c5ePAgpUuXlrukfNNoNIwbN44jR45w+fJlHB0d5S5JbxhsaDdv3szoMWOo4PARXy5YTS2XJjrpR61SsWvzStZ9P5PKDg7s3Bmgt3No586dy8KFCzl48CC1a9eWuxytpaam0qlTJ8zNzTl9+jQWFhZyl6QXDO6WqUqlYvTo0QwaNIjufUezcf9FnQUWwMTUlN6DP2fr4WtgZkWDhg3Zv3+/zvrLr9OnTzN37lzmzJljFIGFjJtT69atIzQ0lOnTp8tdjt4wqJE2NTUVb29vDh48xLyftuPq7lGg/aenp7Hkm7Ec2OWHj48PgwYNKtD+3yUtLY169epRtmxZ/P395S5Hctu2bWPy5MlcvHiRunXryl2O7AwmtCqVCs/evTl69Bjf+e6n7ictZKlDo9Gw5rsZbFq5iC1btuDt7S1LHf+2ePFi5syZw6lTp3BwcJC7HMlpNBo8PDxIS0vj/PnzRvlMPS8M5t2PHz+eg0EHZQ0sgEKhYPTk+fQZOpHBgwdz/Phx2WqBjLvF8+fPZ8KECUYZWMj4b75o0SKuXLlilFcSeWUQI+2WLVsYOHAgi9fuoZX7Z3KXA2T89p/xuTeXzx3n2tWrsm21Mn36dNasWcPly5cN7nlsXo0dO5br169z48aNQj3a6n1o7969S9169fDoM5IJ3yyTu5w3JCUmMKhrAypXLMeJE8cLdLUMZGwL4+DgwNixY/nf//5XoH3L4e+//6Z58+bs2rWL7t27y12ObPT+19WYMWMpX9GRsV8tkruUt1hZ2zD3p+2cOnWSTZs2FXj/vr6+qNVqhgwZUuB9y6Fq1ap07tyZ77//Xu5SZKXXof3ll184duwoU+avQqnUz7nAzjVd6NF/DFOmTiUuLq5A+960aRMeHh4GO+spP/r378+ZM2e4c+eO3KXIRm9Dq9FomDNnLu26eFKnQTO5y8nRyElzSE19zcqVKwuszxs3bnDt2jU8PT0LrE994OrqStmyZdm+fbvcpchGb0N76NAhrl8PYfDYr+Uu5b1si5Wg54Cx/PDDcpKTkwukT39/fxwcHPjkk08KpD99YWJigoeHBwEBAXKXIhu9De3adev4pHlbPqpuGLN7eg/+nOiYaPbt21cg/R0/fpw2bdoU+M0vfdCmTRtCQ0P5559/5C5FFnoZ2pcvX3L40CE69Rgodym5ZleqDE1aurN5yxad9xUfH8/ly5dp3ry5zvvSR40aNcLCwoLg4GC5S5GFXoY2KCgIUOjNM9nccuvmxfFjx0hMTNRpP2fOnCE9PZ2mTZvqtB99ZWlpSf369Tl58qTcpchCL0N74sQJatdvgqWV9pMFHvx9i+9mT2Bg14YSVJazhs3akpaWxunTp3Xaz82bNylTpgylSpXSWR9PnjxhwoQJpKenv/M1kZGR7Nq1i+XLl/Pw4cN8vyY/atSowc2bNyVrz5DoZWhPnz5DvUaukrT19PEDzv1xmFfRLyRpLyelSpelchUnnYc2LCyMjz76SGftq9Vqxo0bx/bt21Gr1dm+ZvPmzQwePBhHR0cmTJiQ7Zk9uXlNflWpUoXQ0FDJ2jMkerdHVGpqKg8fPqCKRIvZm7XpzKHArVy/fF6S9t7nw2of6/wv0507d3Qa2tWrV/Py5ctsv6fRaBg4cCAJCQkEBgZmu8Y1N6/R1kcffUR0dDQvXrzQ6RWHPtK7kfbu3buoVCoqfSjd3kwFOTHDwdGJ0NAwnfbx7NkzPvjgA520fevWLUJCQujRo0e231+5ciV//fUXa9aseWcYc/MabWW+/8jISJ20r8/0LrRRUVEAlLSX8C+lQpH1aOTc74dZuXgaxw7slK79f7ErVSbrPehKfHw8NjY2krf7+vVrZs+ezaJF2U8ZDQkJYeHChYwZM+adW9nk5jVSyDxzKD4+Xmd96Cu9uzxOSEgAMub1Skmj0bB66XQunjlB5LPHbFr1LaeO7WfOcmkf0VhZ2xKfoNu/SImJiTpZ0TN//nzGjBnzztPy1qxZg0ajwcHBgfHjx/Po0SPq1KnD5MmTs6ZS5uY1Usj8pVUYQ6t3I+3r168BMDMzl7Td2JiXuLb3YMPecwSeuscnzdtxKHArF04dlbQfc4sivNbxuTSvX7/G3Fza/z6Zj09atWr1ztdcvnyZUqVKoVar+fbbbxkzZgx+fn5069Yt6y5zbl4jhczL7pSUFMnaNBR6F1orKysAkpOlfdZZ3K4UNepkPPYxN7fgM+8RAFw4+Zuk/SQnJWAt8VXCf1lZWZGUlCRZe69evWLVqlV8880373xNbGws9+/fp0WLFnz66adYW1vj7u7OkCFDuHnzJr/88kuuXiOVzGfhujiaU9/pXWgz/yckxut2xUyjlu0xVSqJinwqabuJCXE6/4tka2ub9TFCCvPnz0ehUDBv3jxmzJjBjBkzOHo04wpk9uzZ+Pv7Exsbi0ajeevSuVGjRkDGAobcvEYqme9fhFYPVKpUCYBn/4TrtB8b22JYWFhS6cOqkrb79NGDrPegK0WLFpV0GWCJEiV4/fo1t27dyvrn+fPnANy+fZtHjx5RsWJFbGxsiIiIeONnGzbMuHqxsrLK1Wukkvn+C9OyxEx6dyOqQoUKWNvYEH4/TKdL8l5GRZCUGE+9Ri0lbffRgzA+dtbtaQSVK1fm0aNHkrWX3faky5cvZ8GCBQQEBGR9fm7SpAnXr19/43WZk/abNGmCQqF472uk8vDhQ0xMTKhYsaJkbRoKvRtpFQoFNT+uye2QvyRtNyU5iZTk//85cMvapXTpNYgGTdtI1odarSbsxhWd7zvs5OTEvXv3dNpHdr799lueP3/O7t27s7529OhRWrVqhaura65fI4V79+7h4OBAkSJFJGvTUOjdSAvQunUrAnbvlay97n1H8uj+Hfp3csHdw5tnTx5iW6wEX85fJVkfAHduXeVVzEtat24tabv/5eTkxOrVq9FoNAW6NK9SpUqsXbuWOXPm8OzZMyIiIoiOjmbz5s15eo0U7t69q7cnPeiaXm7sdvz4cdq1a8cvJ+9SwUG6k8FfRkXw/NkTKn9UXZLFCP/lt2IBuzf9RGRkhE7DdPny5axVLtWrV9dZP+/y+vVrHjx4QMWKFd/5OTU3r9FG48aN8fLyYv78+ZK3re/07vIYMrYUKVPmAw4FbpW03ZL2H1C9dgOdBBbg8N6teHn11vnoV7duXUqUKKHzhQnvYm5ujpOTU45hzM1r8uvZs2fcu3dP51c0+kovQ6tUKunb15ug3RtRSfhAXpdCLp3lwd1Q+vXrp/O+TExMcHV15dSpUzrvSx+dPHkSCwuLQrueWC9DCxkbUz9/9oSjBwxjL6CNPy+kUePGBbZnU4cOHfj9998lfV5rKIKCgnB1ddXrY0d1SW9D6+joiJeXF34/LyA9PU3ucnJ08+qfnAk+yMwZMwqsT09PT9RqNQcOHCiwPvVBTEwMJ06cKJArGn2lt6GFjNk4Tx8/YMeGH+Uu5Z3UajXLZo6jZUtXOnbsWGD9lihRgi5duhS6XQl/+eUXzMzM+Owzw9qKSEp6HdoqVarw9bRprF8+h8cP78pdTrZ2blzBnVtXWbny5wLfGXHo0KGcOXOm0Gy7olarWb9+PZ6enjpZmmgo9PKRz7+lpqbSpElTUtLB55ezmJvrz2ngt0P+YniP5syY8U2Ok+11qX79+jg4OLBu3TpZ+i9Ie/fuZdSoUdy8eRMnJ93OOtNneh9ayHiQ7lK/Pi3dPJj53Ua92Os3KvIpw7s3pYZzNY4cOSzbKW47d+7E29ubU6dOUbWqtPOo9YlaraZNmzbUrFmTHTt2yF2OrAwitABHjhyha9eueA76XPbT8+JjYxjl6Yop6Zw+fYqSJUvKVotaraZevXqUKlXKqD/fbt68ma+++oqrV69So0YNucuRlV5/pv03d3d3Nm7cyI4Ny1k2c/w7dwnUtajIp4zydCU54RVHjhyWNbCQ8cx21apVBAcHs3//fllr0ZXo6GgWLlzIhAkTCn1gwYBG2kx79uyhb9++NGvTmW+WbsDGtliB9X075C+mje5JMVtrjhw5rFcrTIYMGcLhw4f5/fff37ldjKEaPXo0586d4/bt24Vy/ex/GcxIm6lHjx4cPXqUW1fOMbBzfW5c0f3WqGqViu3rf2BYj2Z8XN2J06dP6VVgAZYtW4ZSqWT8+PEY2O/hHPn7+7Nnzx58fHxEYP+PwYUWoEWLFly9eoXqTh8xrHszFk0bqbPNyG9cOc9gj0as/PZLZs2cyeHDh/RyJLOzs2PHjh2cOHGCVaukXb0kl9DQUL788kumTp1aoM/A9Z3BXR7/1/bt2/nii8nExcfTve8o+gz9H/YflNe63at/nmLT6m85c+Igrq6tWLVqpUF8nlq6dCnTpk3D19eXzp07y11OvkVERNC5c2cqVapEcHAwSqVeriKVhcGHFjI2+VqzZg3Lln1HVNRzGjZri1tXLz5p3o4y5XJ3GatWqQi7eYUzwQc5sncb4ffv0LRZM2Z88w0dOnTQ8TuQ1oQJE1izZg3bt2+XdOF5QYmPj+fTTz8lJSWF06dP63T/ZENkFKHNlJqayoEDB9i0eTO//fYbqSkpVHBwpIpTLSo5OlGyVBksrW0oYmlFclIica+iiXj6iMcP7hB28wpxr2L44IOyODs78dFHH+Hj4yP3W8oXlUqFl5cXv/32G1u2bDGo1TDR0dF4e3sTERHB2bNndb7fliEyqtD+W3JyMufOneP06dPcunWLsLA7vHjxgoSEBJKSErGysqZEiRJUrFQRp2rVqFWrFm3atKFGjRp8//33zJs3j6dPn+pkPWhBSE1NpV+/fhw4cIA1a9YYxKXykydP8PT0JC0tjcOHDxfqWU85MdrQauPly5eUL1+etWvXMnCg4Rxs/V8qlYrx48ezbt06pk+fzrhx4/RiNll2Lly4wLBhw7C3t+fw4cOUK1dO7pL0lkHePda1kiVL0q1bN3x9feUuRSumpqasWrWKxYsXs2jRIvr27Ut0dLTcZb1Bo9Hw448/4uHhQcOGDTl58qQI7HuI0L7D0KFDOXXqFLdv35a7FK198cUX/PHHH4SFheHq6srevdJtmqeN0NBQPDw8WLx4MYsXL2bfvn0UL15c7rL0ngjtO7i5ueHg4ICfn5/cpUiiSZMmXL16lS5dujBy5Eh69uwp26HMsbGxzJkzhzZt2vD69WvOnTvHpEmT9PbSXd+I0L6DiYkJgwcPxs/Pj1QdH6hVUEqUKIGPjw+nT58mNjYWV1dXhgwZ8tbm4rqSOYfYxcWFbdu28eOPP3LhwgXq169fIP0bC3EjKgdPnjyhcuXK7Nixg549e8pdjqTUajV79+5lwYIFXLlyhcaNG+Pp6Um3bt0kPWpDpVJx8uRJdu3axYEDB7CxseF///sf48aNK5RHekhBhPY9OnXqhEaj4dChQ3KXojNbt25l3LhxpKSkoFAoaN68OS1atKBFixZ8/PHHeV4r/PTpU06dOsXp06cJDg4mMjKSJk2aMGDAAPr376+Ts3ULExHa99izZw+enp7cv38fBwcHucvRiW+++QZfX1+uX7/Ovn37OHLkCL///jtRUVEUKVKEKlWq4OjoSKVKlShevDg2NjaYm5uj0WiIjY0lISGBiIgI7t69y71794iOjsbCwoImTZrQtm1bevfubdQL9AuaCO17pKenU7FiRUaOHMns2bPlLkdy6enpODg4MGTIEObNm5f1dY1Gw40bN7h8+TJhYWGEhYURHh5OTEwMiYmJpKamolAoKF68OLa2tpQpUwYnJyecnZ35+OOPadSoUaHd4lTXRGhzYerUqfj7+/Pw4UNMTU3lLkdSmVcS9+7do3LlynKXI+SCCG0u3LlzB2dnZw4ePGhwiwfep3379pibmxe6/ZMNmQhtLrm6ulK6dGl27doldymSuX//PlWrVmXv3r107dpV7nKEXBLPaXNp6NCh/Prrr1knpBuDtWvXUq5cOTp16iR3KUIeiNDmUq9evbC2tpb8nFW5vH79mo0bNzJs2DCj+5xu7ERoc8nS0pI+ffqwfv16o9iD6ZdffiE6Opphw4bJXYqQR+IzbR6EhIRQp04dTp06RfPmzeUuRyutW7emePHiBAYGyl2KkEdipM2D2rVr4+LiYvBL9sLCwvjjjz8YOXKk3KUI+SBCm0dDhw4lICCAV69eyV1Kvq1du5aKFSvi5uYmdylCPojQ5lHfvn1RKBT4+/vLXUq+pKamsnXrVkaOHCluQBkoEdo8KlasGD179jTYS+SdO3cSExPDoEGD5C5FyCdxIyofTp48iaurK5cvX6ZevXpyl5MnzZs3p1y5cuzcuVPuUoR8EiNtPrRs2ZLq1asb3Gh7+/Ztzp49K25AGTgR2nwaNGgQW7duJSkpSe5Scm3VqlU4OjrSunVruUsRtCBCm08DBw4kKSmJPXv2yF1KriQnJ7Nt2zZGjhwp2wHYgjTE/718KlOmjEFts+rv709iYqJB7+MsZBCh1cLQoUP5448/ZNvVMC/Wrl1Lz549xbk4RkCEVgvu7u44ODiwceNGuUvJ0bVr1/jzzz/FDSgjIUKrBRMTEwYNGsTGjRtJS0uTu5x3Wr16Nc7OzrRo0ULuUgQJiNBqaejQobx48UJvd35ISEjA39+fkSNHis3AjYQIrZYqVqxIu3btWL9+vdylZGvbtm2kpaUxYMAAuUsRJCJCK4GhQ4dy+PBhHj16JHcpb1m3bh2enp7Y2dnJXYogERFaCXz66aeUKlVK725I/fnnn1y+fFncgDIyIrQSMDc3p3///qxfvx6VSiV3OVnWrl1LrVq1aNKkidylCBISoZXI8OHDefLkCcePH5e7FCDjZLqAgAAxyhohEVqJODk50bRpU72ZIbVlyxY0Gg19+/aVuxRBYiK0Eho2bBh79+4lKipK7lLw9fXF29tbHNJshERoJdS7d2+srKzYsmWLrHWcOXOGq1eviktjIyVCKyFLS0u8vLzw8fGRdZvVtWvXUqdOHRo0aCBbDYLuiNBKbOjQoYSGhnLu3Lm3vqdWq3Xe/6tXr9izZw9jxozReV+CPERoJdagQQPq1av3xg2ps2fPMnjwYHr16iVpX1999RVjxozh2rVrWV/z8/PDxMSEPn36SNqXoEc0guRWrFihsbKy0ixcuFBTrVo1DaBRKBSapk2bStpP//79NYAG0NSvX1/j5+enqV69umb06NGS9iPoF6XMvzOMilqt5sSJE5w8eZKkpCRmzJiRdUms0Wgk35omPj4+69+vXLnCsGHDUKvVVK9enZCQEGrXri1pf4J+EJfHEnjy5Anz5s3DwcEBNzc39u7dC4BKpXrjhpTUoY2Njc36d7VandXf/v37qVOnDnXr1mXdunUGtY+V8H4itBK4cOECs2bN4smTJwDvXFsrdXji4uKy/Xpm/9evX2fkyJEsWbJE0n4FeYnQSqBHjx58++23712vmpKSImm//748zo6JiQmfffYZM2fOlLRfQV4itBKZOnUqo0aNyvGojdTUVEn7TEhIeOf3zMzMqF27Nlu3bhW7LxoZccKAhFQqFV26dOH48ePZXiKbm5tLGtwSJUpkexCYmZkZZcuW5eLFi2IjNyMkQiux+Ph4GjVqxN27d98KrkKhID09XbKRz8LCgtevX7/xNVNTU2xtbfnzzz+pWrWqJP0I+kVcN0nM1taWo0ePUqJEibculTUajWSfa1Uq1VuBVSgUmJqacvDgQRFYIyZCqwPly5fn4MGDmJmZvTWqJicnS9JHdp9nFQoFAQEBYtG7kROh1ZH69euze/fut76uy9AuX74cDw8PSdoX9JcIrQ517tyZxYsXv/EoSKpntf8OrYmJCV9++SXjx4+XpG1Bv4nQ6tjkyZMZMWJE1p+lGmkTExOBjEviXr16sWjRIknaFfSfCG0B+Pnnn3FzcwOkH2mbNm3Kpk2bxEbkhYgIbQFQKpXs2bOHOnXqSHb3OCEhgRo1arB//34sLCwkaVMwDAXynDYhIYGwsDCio6OzJgOYmZlha2tLpUqV+PDDD1EqjXvBUXx8PCdOnODvv/+mVKlSbz2uyQtra2uePHlC/fr1adWqldH/txPepJP/28+ePePAgQMEBwdz+vQZHj/Oeed9c3NznJ2r07p1K9q1a4ebm5tRjB7Pnz/Hz8+PPXv2cOnSJZ3sXGFtbY2bmxve3t54eHhgZmYmeR+CfpFspFWr1ezbt491Pj4c/e03zC2KULdhc+o1cqWKU00qV3GmeEl7bItm7A6Ynp5GQlwsEf+EE37/DrdDLnLpXDB/3w6hWPHiePXuzbhx46hRo4YU5RWo+Ph4FixYwPLly7GwsKBt27Y0adIEZ2dnypQpk+P85NxKTU0lPDyc69evc/LkSc6fP0+5cuVYtmwZnp6eErwLQV9JEtodO3Ywd+48wsJCadqqI516DKClWzfMLYrkua2oiH84vHcbB3b5EX7/Dp96eDB/3jyDCa+/vz+TJk0iOTmZESNG4OHhUSBXDREREfj4+LB//36aN2/OmjVrqF69us77FQqeVqENCwtj1KjRnDz5Bx08+jJo7DQqfyTNXxS1Ws3Jo/vw/XEu9+/cZNKkScycORMrKytJ2peaSqViypQpLF++nB49ejB69GiKFStW4HXcunWLJUuWEB4eTkBAAB07dizwGgTdyndoN2zYwLjx46lcxZmp81fzcd1PpK4NyAjvL1vXsOa7b6hQrhy7du3Uu1E3MTGR3r17c+zYMWbNmkX79u1lrSctLY2FCxdy8OBBli9fzrhx42StR5BWnkObnp7O6NGj8fX1ZcDoLxn1xTxMC+DuZeTTx8z4vA9hN6+wfds2vZmup1Kp8PDw4OzZs3z33XfUqlVL7pKy+Pn5sWrVKvz8/Bg4cKDc5QgSyVNoU1JS8PLy4rffjjL/5x20aNdVl7W9RZWeznezPydw+zrWrFnDsGHDCrT/7HzxxResXLmS1atX6+VGaitXrmTr1q0cOXKE1q1by12OIIFcD5EqlYreXl4E//4HK7YdpXb9prqsK1umSiVT56+iuJ09I0aMwMLCgv79+xd4HZl27tzJDz/8wPz58/UysACjR4/m0aNH9OrVi9u3b2Nvby93SYKWcj3Sjhw5ki1btvLT1t+o06CZrut6r5+//Qr/9d9z4MABWT5DJiQk4OTkRKNGjZg+fXqB958XSUlJ9OrVi08//ZS1a9fKXY6gpVxNY9y4cSM+Pj7MW+GvF4EFGPvlItp29sTbu2/WLogFaeHChSQkJBjE8RtWVlaMHz+e9evXc/HiRbnLEbT03pH2zp071HNxoUf/sYyftrig6sqV5KREBnVrSMVypfk9OLjAJs2/fPmSChUqMGbMGLy9vQukT21pNBqGDRtG+fLlCQoKkrscQQvvHWnHjBlLxcpVGT1lfkHUkyeWVtbM+2k7Z86cwc/Pr8D63bRpE2ZmZnz22WcF1qe2FAoFffv25UlkUUsAACAASURBVPDhwzx+/FjucgQt5BjaXbt2ERx8gqnzV6FU6uec1mo16tJrwFimTv3yjR33dSkwMJBWrVphaWlZIP1JpWXLllhbW7Nv3z65SxG08M7QajQa5s6dh1vX3tRy0e89h4ZPnM3rtDR+/vlnnfeVnJzM+fPnady4sc77kppSqaRBgwYEBwfLXYqghXeGNigoiJs3bzBwzLSCrCdfbIsWp9fA8fzww3Kdn1tz+/Zt0tPTcXJy0mk/uuLk5ERISIjcZQhaeGdo1/n40KiFGx85688Mn5z0Hjye2LjYrMOvdCUiIgKAMmXK6LQfXSldunTWexAMU7ahff78OYcPHaJzT8OZ+laiZGmaunZg8+YtOu0nc2+mIkXyvoJJH1haWma9B8EwZRvaQ4cOYWJiimt7/Zjfm1vtuvbmxInjOZ5xo63MJ2SGvCeTOFTCsGUb2hMnTlC7QVOKWOrnMrh3+aR5O9LT0zl9+rTcpQiCzmQb2jNnzlL3k5YFXYvW7EqVoXIVJxFawai9FdrU1FQePnxAFaeactSjtQ+rfkxoaKjcZQiCzrwV2rt376JSqaj0YTU56tGaw0fOhIaGyV1Grl29epUNGza89fWIiAi+/fZbGSoS9N1boY2KigKgpP0Hknf2+MHf7NjwIz7L53D290OStw9QslQZXrx4oZO2daFu3bpER0fj6+ub9bWIiAi++eYbg5nXLBSst0IbHx8PgJW1jaQdLZv1OfOmDqXjZ/2oXb8pEwd1ZvNq6RcgWFnbEhcfJ3m7ujR58mRiYmLw9fXNCuzMmTOpVKmS3KUJeuit0GYehGxmZi5pRwf3bKZJS3eKlShJoxZuVP6oOr8fkX4ihJm5BWlabAQul8mTJ/P06VNGjBghAivk6K3QZu52mJws7QP4HzYG0aP/aABuXv0TjUZDaoo0h1H9W1JiPNYSXyUUhIiICB4+fIiLiwvHjh2TuxxBj70V2qJFiwKQKPElZp0Gzbh84Q9m/a8/jx7coVyFymiQ/iF/UmI8tra2krerSxEREUyfPp2ZM2cye/ZsoqOjs705JQiQTWgzL8uePnkoaUcrFk7l14ANTF/sQ8fP+mGmow28/3l0HwcHB520rQv/Dmxm3ZMnTxbBFd7prdCWL18eaxsbHt2X7rFJ6PVLbFm7lF4Dxr556oAOptM9fnAHZ2fDWYGjVqvfCGymyZMn4+zsLFNVgj57K7QKhYJaNWtxK+QvyTrJnA75x297UaWn8+fpY/x96xpxsTE8fvA3Tx8/kKQftVpN6PXLerszYnbKlSv3ziuDpk0LfsdLQf9lO42xdetWXD4n3ULpyh9Vp1P3/uz196Fzowo8Cb/Hp72H8iLyKYHb11Gu4oeS9HPn5hViX0WL/X0Fo5btvsft2rVj0aJFPH54l4qVP5Kko9k/bGbCN99hW6x41tY1PQeMwbZYCUnaBzj7+yFKly5DzZqGOQVTEHIj25HW1dWVcuXKczhwq6SdlShp/8ZeU1IGFuDI3m14efXW6bK5zAOcVSqVzvrQJbVaLQ6hNnDZhtbU1BRv7z4E7d6IKj29oGvKl6sXT/PgbqjOTxwoXjzjfF1drtnVpbi4OFlO8xOk887tZsaPH09U5FOO/OpfkPXk28aVC2ncpAkNGjTQaT+Ojo4AhIeH67QfXXn06BFVqlSRuwxBC+8MbaVKlejTpw8bf15AenpaQdaUZzeunOfc74eZNXOmzvtycHDAzs7OYDdHu379Oi4uLnKXIWghx32PZ8+eTcTTR/iv/6Gg6skztUrF0pnjaNWqNR06dNB5fwqFgo4dO3Ly5Emd9yW1qKgobt26JQ6aNnA5hvbDDz9k+tdfs/7HuTy6f6egasqTHRt+5F7odVau1P2ex5m8vb25cuUKDx8+LLA+pbBv3z5KlCgh+6HXgnbeeyzI1KlT+bhGDaaP683r1JSCqCnXbl79k5VLpjF79myqV69eYP126NCBqlWrGtQJdLGxsfj7+zNq1CiD3UlSyPDe0JqbmxMQsINnTx4w/8therOT3/NnT5g2uidt2rThyy+/LNC+TUxM+Pnnnzl69CiXLl0q0L7za/Xq1VhYWDB16lS5SxG0lKujLh0dHdmzezcngnbxw9yJuq7pvWJjXjJhQAfsihfFf/t2TExy9TYk5ebmRqdOnVi6dCkpKfp1BfJfN27cIDAwkKVLl2at4hIMV64PlQYICAigb9++ePQZzpS5P2NiaqrL2rIV+fQxEwZ2ID0lkTNnTlOhQoUCryHTgwcPaNiwIXXr1mXRokWy/PJ4n8jISAYNGkSDBg0ICgoy6P2ahQx5+lvWu3dvdu/ezYHdG5k6sjvxca90VVe2bly5wPAezbA0M+Hs2TOyBhYybtQFBgZy6tQpVq5cKWst2UlMTGTSpEmULl2aHTt2iMAaiTwPDR4eHhw/doy7Ny/Rv1M9rv11Rhd1vUGtUrFl7VJG9GpB3To1OXXqJOXLl9d5v7nRokULfHx82LJlC4sXL9ab6Y0REREMHz6c2NhYDhw4IC6LjUi+rueaNWvG1atXqFOrBiN6tmD+lKHEvHwudW0AXPvrDAO7NmDtsm+YP28eB4OCKFFC2jnL2howYAC7du3iwIEDTJw4kbg4eTeWCwkJYdCgQZibm3P+/HkqV64saz2CtPL0mTY7u3bt4n//m0hMTAwe3iPoM2wiH5TTblMyjUbD5Qt/sGnlIs6f/I22bduxcuXPen+85KVLl+jWrRvJycmMHj0aDw+PAv2c++rVK1atWsW+fftwd3dnx44dBrf1jvB+WocWICkpCR8fH5YuXcazZ09xaexK+259+KR5u1yvlVWlp3P7+l+cDT7EkX3bePzwHi1dXZnxzTe0a9dO2xILTGxsLHPnzmXFihU4Ojri7e1N27ZtdfpsNCIign379hEQEICNjQ1LlizB29tbfIY1UpKENtPr1685dOgQmzZv5vDhwyQnJVGuggMfVvsYhyrO2JUsnbUcLy3tNYlxsTx98pDHD+4QdvMKiQnxlC9fgdKl7WndujXfffedVKUVuNDQUGbNmkVgYCCmpqbUq1cPJycnypQpg6kEd91TUlIIDw/n5s2bhIWFYW9vz6hRo5gyZQo2Noa3G6WQe5KG9t9ev37N+fPnOX36NLdu3SIs7A4vX74kJiYGjUaDhYUFtra2ODg44ORUjdq1a9OqVSucnZ2ZOHEi+/bt4969ewY/Wjx//pz9+/cTHBzMX3/9RUREBElJSVn7S+eHlZUVxYoVw9HRkfr169OxY0fatGmDubm0e1UL+klnodXGjRs3qFWrFsHBwbRq1UruciQzZswYTpw4we3btw3+l5EgH/2bDQDUrFmThg0bGtUWosnJyezYsYPhw4eLwApa0cvQAgwdOpTdu3fz6lXBTuDQlT179hAfH0/fvn3lLkUwcHob2j59+qBQKNixY4fcpUhiw4YNdO3alQ8+kP40QqFw0dvQFi1alJ49exrFJfKDBw/4448/GDp0qNylCEZAb0MLMGTIEC5evMi1a9fkLkUrvr6+lC5dGnd3d7lLEYyAXoe2ZcuWVK1aFT8/P7lLyTe1Ws3mzZsZMmSI2LpUkIReh1ahUDB48GC2bt1Kamqq3OXky5EjR3jy5AmDBg2SuxTBSOh1aAEGDx5MbGws+/btk7uUfPH19c26YhAEKeh9aD/44APc3d0N8obUy5cvOXDggLgBJUhK70MLGc9sjx49anAbhG/atAkLCwt69OghdymCETGI0Hbt2pXSpUuzadMmuUvJk02bNtGnTx+srKzkLkUwIgYRWqVSSd++ffHz80OtVstdTq5cuHCBkJAQcWksSM4gQgswfPhwwsPDCQ6W7txcXdqwYUPWHGpBkJLBhNbJyYnGjRvj6+srdynvlZyczM6dO8UoK+iEwYQWMmZIBQYGEhMTI3cpOdq5cydJSUlicYCgEwYVWi8vL8zMzNi+fbvcpeTI19eXTz/9FHt7e7lLEYyQQYXWxsaGnj17sm7dOrlLeae///6b06dPM2TIELlLEYyUQYUWMp7ZhoSEcOXKFblLydaGDRsoX748bm5ucpciGCmDC22zZs2oXr26Xs6QSk9PZ/PmzQwePFiSzdsEITsGF1qAQYMGsXXrVpKTk+Uu5Q2HDh3i2bNnDBw4UO5SBCNmkKEdOHAgiYmJ7N27V+5S3rBhwwZat25NlSpV5C5FMGIGGdoyZcrQqVMnvXpmGxkZSVBQkHg2K+icQYYWMp7Znjhxgnv37sldCgCbN2/GysoKDw8PuUsRjJzBhrZTp06ULVuWzZs3y10KAH5+fvTr108sDhB0zmBDq1Qq6d+/Pxs2bJD9eMmzZ89y+/Zt8WxWKBAGG1rIuET+559/OHbsmKx1+Pr6Urt2bVxcXGStQygcDDq01apVo1mzZrI+s01ISGDXrl0MHz5cthqEwsWgQwsZM6T27t1LVFSULP0HBASQmpqKl5eXLP0LhY/Bh7ZXr14UKVJEtkUEGzZsoHv37pQqVUqW/oXCx+BDa21tTe/evVm/fn3W1zQaDb///jsDBgxAqkMBg4ODWbRoEU+fPs36WlhYGOfOnRPPZoUCpZdHXebV+fPnadKkCQcPHuTKlSusXbuWR48eARmn1FtaWmrdx8aNGxk8eDAmJia4u7szfPhwTp8+za5du3jw4IGYaywUGIPf8j4tLY2nT59iY2ND586dUSqVbxzYnJKSIkloU1JSUCqVpKenc/ToUQ4fPoxCoaBRo0aEhYVRo0YNrfsQhNww2MvjsLAwvvrqKz744AN69uxJcnIyGo3mrRPWpTqZIDk5GROTjP9c6enpaDQa1Go1f/31Fx9//DF16tRh3bp1xMfHS9KfILyLQY60N2/epF69eqhUqqzdGd81wUKqlUCpqanZHgad+Uvixo0bjBw5kvXr13Pu3DlxuSzojEGOtB9//DE+Pj652k41JSVFkj7f145CoaB48eLs2LFDBFbQKYMMLWQsz/v666+zLlnfpaBCa2JiQlBQEI6OjpL0JwjvYrChBZg/fz6enp6YmZm98zVShvZdN9oVCgUbNmygadOmkvQlCDkx6NAqFAr8/PyoU6fOO4MrVWgzb3T9l4mJCbNmzaJfv36S9CMI72PQoQUoUqQIhw4domzZstke2izlSPvfz9BKpZLu3bszc+ZMSfoQhNww+NAClCpVikOHDlGkSJG3PuPqKrRmZma4uLiwdevWbO8qC4KuGEVoAWrUqMHu3bvf+JpCoZAstElJSVmXx0qlkjJlyrB//34sLCwkaV8QcstoQgvg7u7O2rVrs/5sYmIiaWgz2zQ3N+fQoUOULl1akrYFIS+MKrQAw4YNY8KECZiamqJWqyUPrUKh4Ndff6VmzZqStCsIeWV0oQX47rvvcHNzQ6PRSHr3GGD16tW0bdtWkjYFIT+MMrSmpqbs3LmT2rVrSxZatVrN5MmTxQ4VguwMcu5xbi1YsICDBw+yceNGXr9+ne92rK2tqVKlCgMGDCA9PT3bR0uCUFCMYj1tpufPn+Pn58eePXu4dOlSruYm55W1tTVubm54e3vj4eGR42wsQdAFowhtfHw8CxYsYPny5VhYWNC2bVuaNGmCs7MzZcqUkWQCf2pqKuHh4Vy/fp2TJ09y/vx5ypUrx7Jly/D09JTgXQhC7hh8aP39/Zk0aRLJycmMGDECDw+PAnl2GhERgY+PD/v376d58+asWbOG6tWr67xfQTDY0KpUKqZMmcLy5cvp0aMHo0ePplixYgVex61bt1iyZAnh4eEEBATQsWPHAq9BKFwMMrSJiYn07t2bY8eOMWvWLNq3by9rPWlpaSxcuJCDBw+yfPlyxo0bJ2s9gnEzuNugKpUKLy8vzp07x5o1a6hVq5bcJWFmZsasWbOoVKkSn3/+Oba2tuKMWkFnDC60U6dO5ejRo6xevVovAvtvgwcPJikpieHDh1OpUiVat24td0mCETKoy+OdO3fi5eXF/PnzcXd3l7ucbKnVaqZNm8bVq1e5ffs29vb2cpckGBmDCW1CQgJOTk40atSI6dOny11OjpKSkujVqxeffvrpGwsYBEEKBjONceHChSQkJDBmzBi5S3kvKysrxo8fz/r167l48aLc5QhGxiBG2pcvX1KhQgXGjBmDt7e33OXkikajYdiwYZQvX56goCC5yxGMiEGMtJs2bcLMzIzPPvtM7lJyTaFQ0LdvXw4fPszjx4/lLkcwIgYR2sDAQFq1aiXJ8R4FqWXLllhbW7Nv3z65SxGMiN6HNjk5mfPnz9O4cWO5S8kzpVJJgwYNCA4OlrsUwYjofWhv375Neno6Tk5OcpeSL05OToSEhMhdhmBE9D60ERERAJQpU0bmSvKndOnSWe9BEKSg96FNTEwEMvY3NkSWlpZZ70EQpKD3oc18ImXIewsbwFM1wYDofWgFQXhToQ6tWq0mLi5O7jIEIU8KdWgjIyNZsWKF3GUIQp4U6tAKgiESoRUEAyNCKwgGxuB2rtDGr7/+SmhoaNafExMTuX37NkuWLHnjdUOHDqVkyZIFXZ4g5EqhCm3jxo3fODjrxYsXpKSk0LNnzzdeV7Ro0YIuTRByrVCFtnTp0m8cT2lpaUnRokVxdHSUsSpByBvxmVYQDIwIrSAYmEIdWgsLC6pUqSJ3GYKQJ4U6tHZ2dnh5ecldhiDkSaEOrSAYIr0PbeYBziqVSuZK8ketVotDqAVJ6X1oixcvDmRsVm6I4uLiZDnNTzBeeh/azGeo4eHhMleSP48ePRI3uwRJ6X1oHRwcsLOzM9jN0a5fv46Li4vcZQhGRO9Dq1Ao6NixIydPnpS7lDyLiori1q1b4qBpQVJ6H1oAb29vrly5wsOHD+UuJU/27dtHiRIlZD/0WjAuBhHaDh06ULVqVYM6gS42NhZ/f39GjRplsDtJCvrJIEJrYmLCzz//zNGjR7l06ZLc5eTK6tWrsbCwYOrUqXKXIhgZgwgtgJubG506dWLp0qWkpKTIXU6Obty4QWBgIEuXLhXL/ATJGcRRl5kePHhAw4YNqVu3LosWLcLERP9+50RGRjJo0CAaNGhAUFCQQe/XLOgngwotwKlTp2jXrh19+vRh/PjxcpfzhsTEREaMGIFSqeTs2bNilBV0Qv+Gqvdo0aIFPj4+bNmyhcWLF+vN9MaIiAiGDx9ObGwsBw4cEIEVdMbgQgswYMAAdu3axYEDB5g4caLsG46HhIQwaNAgzM3NOX/+PJUrV5a1HsG4Gdzl8b9dunSJbt26kZyczOjRo/Hw8CjQz7mvXr1i1apV7Nu3D3d3d3bs2IGtrW2B9S8UTgYdWsh4Hjp37lxWrFiBo6Mj3t7etG3bVqfPRiMiIti3bx8BAQHY2NiwZMkSvL29xU0noUAYfGgzhYaGMmvWLAIDAzE1NaVevXo4OTlRpkwZTE1NtW4/JSWF8PBwbt68SVhYGPb29owaNYopU6ZgY2MjwTsQhNwxmtBmev78Ofv37yc4OJjr16/z5MkT4uLiSE9Pz3ebFhYW2NnZ4ejoSP369enYsSNt2rTB3NxcwsoFIXeMLrT/FRQURJcuXUhISMDa2jrPP+/n58f48eMNdj2vYHwM8u5xXkRFRWFlZZWvwALY29uTmJhIUlKSxJUJQv4YfWifP3+Ovb19vn8+82dfvHghVUmCoBWjD21UVJQkoY2KipKqJEHQSqEI7b+PAskrEVpB3xSK0Goz0tra2lKkSBERWkFvFIrQajPSApQqVUqEVtAbRh/a6Oho7OzstGqjZMmSREdHS1SRIGjH6EObkJCg9YwlGxsbEhMTJapIELQjQpsL1tbWYnKFoDeMOrRqtZqkpKR8T6zIZGNjI0Ir6A2jDm1SUhIajUaMtIJRMerQZgZNipFWfKYV9EWhCK0UN6LESCvoCxHaXBCXx4I+MerQJicnA2BpaalVO1ZWVmKVj6A3jDq0mQvfzczMtGpHqVRqtYheEKRUKEKr7XYzpqamIrSC3igUoVUqlVq1o1Qq9WZ/ZUEw6tBmBk2K0IqRVtAXRh1aKS+PxUgr6ItCEVox0grGxKhDKy6PBWNUKEKr7VEh4vJY0CdGHdrMz7JqtVqrdlQqlSSnFAiCFIw6tJmXxWlpaVq1k56ervUltiBIpVCEVtvPoyK0gj4x6tBmTl8UI61gTIw6tGKkFYyRUYdWqpE2LS1NhFbQG0Yd2v+OtKmpqURGRhIWFvbOn3n16hX3798nOjqazAMF09PTtV4pJAhSMZrhIzk5mSVLlhATE0NMTAzR0dGEh4djYmJCvXr1SEhIIDU1FYBatWoREhKSbTsRERFUr1496882NjaYmpqSmpqKq6srpUqVws7OjhIlSlCnTh369u1bIO9PEDIZTWgtLS05cuQIFy5cQKFQvDEZ4uXLl1n/bmpqSocOHd7ZjrOzM2XLluXZs2cAb+xYcfLkyaw2VCoVP/74o9RvQxDey6gujydMmIBGo8lx9pJKpcLd3T3Hdrp06ZLjKe8qlQpLS0sGDhyY71oFIb+MKrQ9evR477k9FhYWNG/ePMfXuLu753jzytzcnCFDhlCsWLF81SkI2jCq0CqVSsaOHfvOO70mJia0atUKCwuLHNtxc3PLcb5yWloa48aN06pWQcgvowotwKhRo1AoFNl+z9TUlE6dOr23jaJFi+Li4pJtO0qlktatW+Ps7Kx1rYKQH0YXWnt7ezw9PbN9RJOWlvbez7OZunTpku2IrVKpmDhxotZ1CkJ+KTSZDyONyKVLl2jQoMFbXy9Xrhz//PNPrtr4888/adSo0VtfL1++POHh4WLVjyAboxtpAerXr4+Li8sbwTIzM6Nr1665bqNBgwYUL178ja8plUomTZokAivIyihDCzBx4sQ31tGmp6fn+tIYMm5aubm5vXGJbGpqyqBBg6QsUxDyzGhD6+np+cYJ8CYmJrRp0yZPbXTs2DEr+GZmZgwcOFDrU+UFQVtGG1pzc3PGjBmDUqlEoVDQoEGDPD9Xbd++fdb847S0NMaOHauLUgUhT4w2tJDx+Eej0aDRaOjcuXOef758+fJUq1YNgGbNmlG7dm2pSxSEPDPq0JYrV47u3bsD5Onz7L916dIFgEmTJklWlyBow6hDC/D5559TsmTJbB8B5Ya7uzuVKlWiW7duElcmCPljlM9pAeLj47l58ybPnj0jICAgzzehMqWlpfHnn3/i6enJhx9+SLVq1cSCeEFWRhXa58+f4+fnxy979vDXpUtab52aHRtra9zc3Ojj7Y2Hh4dYHC8UOKMIbXx8PAsWLGD5Dz9gZVGEns1a0qF+I+pVqUpF+9IoJZgMkfw6lbAnjzkfepNfL5zlt0sXKV++HEuXLcPT01OCdyEIuWPwofX39+eLSZNITUpmtvdAhnXogqV5zqt4pPAoKpI52zax8dghWjRvzuo1a97Y8UIQdMVgQ6tSqZgyZQrLly9nZKduzO8/jJJFixZ4HRfvhDJuzY+E/vOYHQEBdOzYscBrEAoXgwxtYmIiXr17c+zoMTZO+oreLfN3k0kqr9PTGLniO7ac+I3ly5eLtbaCThncbVCVSkUfLy8unDlL8LfLaexcQ+6SMFea4TfxK6qVr8Dnn3+Ora2t2IpG0BmDC+3UqVM5+ttRTiz6QS8C+2/TPPsRn5TM8GHDqVSpEq1bt5a7JMEIGdTl8c6dO/Hy8mLblBn0adVW7nKypdao8Vw0mz9u3+DW7dvY29vLXZJgZAwmtAkJCTg7OdGpdn3WfT5Z7nJylJCcjPOo/nT+7DPWrl0rdzmCkTGYaYwLFy4kKT6BBQOHyV3Ke9lYWrJk8CjWr1/PxYsX5S5HMDIGMdK+fPmSihUqsKD/UCZ+ZhgTGTQaDc2njqeEQ0UOBAXJXY5gRAxipN20aRPmSiUjOhrOpH2FQsEkj14cOnyYx48fy12OYEQMIrR7AwP5rEkLrIsUkbuUPOnWqBm2Vlbs27dP7lIEI6L3oU1OTubc+fO0r9dQ7lLyzEyppE1tF4JPnJC7FMGI6H1ob9++TXp6OvWqVJW7lHypV+Ujrr/jhD5ByA+9D21ERAQAFe1zPqNHX5UvaU9EZKTcZQhGRO9Dm5iYCIDVe87f0Vc2lpYk/N97EAQp6H1oM59Ivet8HkNgAE/VBAOi96EVBOFNIrSCYGBEaAXBwBS60J6+eZ0FO7a89fVHUZGMWfm9DBUJQt4UutA2/7gWz2NjmO+/Oetrj6Ii6btknsHMaxYKt0IXWoAfR35OVNwr5vtvzgrsholfUbVcBblLE4T3KpShhYzgPnwegevUz0VgBYNSaEP7KCqS0MePcK1Vl12nfpe7HEHItUIZ2kdRkXgvnofv/6aycdI0Il9FszBgq9xlCUKuFLrQ/juwThUqARmXyiK4gqEodKFVqzVvBDbTjyM/N9iVRELhYnBbqGqrcpkP3vm9jg0aFWAlgpA/hW6kFQRDJ0IrCAZG70ObeYBzukolcyX5o1KpxSHUgqT0PrTFixcHINZAF5LHJMRTvFgxucsQjIjeh9bR0RGAO/8Y5jakd/55TJX/ew+CIAW9D62DgwMl7ew4e/uG3KXky7mwW9SrX1/uMgQjovehVSgUdOjYkV8vnJW7lDx7+vIFf90JFQdNC5LS+9ACeHt7c+rGNUIfP5K7lDzx/S0IuxIlaN++vdylCEbEIELboUMHqn1UlZlbN8hdSq69jItj+b49jBw1iiIGdjKCoN8MIrQmJiasWPkzu04F83vIVbnLyZUZW9ZjVsSCqVOnyl2KYGQMIrQAbm5udO7UifFrfyIpNUXucnJ0IewW6w4dYMnSpRQtWlTucgQjYxBHXWZ68OABnzRsiGv1muycNhsThf79znkc9ZxGk0ZT75OGHAgKMuj9mgX9pH9/63Pw4Ycf8ktgIPsvnOPrjT5yl/OWuKREus79mpIflMF/xw4RWEEnDCq0AC1atMBnvQ9L9+xg7KrlejO98VFUa1OURQAAIABJREFUJC2mfs7zxHj2HzggLosFnTG40AIMGDCAXbt2sfH4YbrMmUZMQrys9Zy7fZNGE0ejsSzCufPnqVy5sqz1CMbNIEML0L17d06eOsWNp4+pNqIfaw/+ikqtLtAaXsTFMnLFd7SYMp76jRtx5txZHBwcCrQGofAxqBtR2YmNjWXu3Lms+GkFNRwqM8mjFz2bu2Jlobtno4+iIvE9EsSK/YFY2dqweMkSvL29xWdYoUAYfGgzhYaGMmvmTAID96I0NaFlrbrUc/yIiqVKozQ11br9xJRk7vzzhAt/3+bq3b8pbW/PyFGjmDJlCjY2NhK8A0HIHaMJbabnz5+zf/9+goODuRESwpN//iE2Lo709PR8t1nEwgK7EnY4VnHEpX59OnbsSJs2bVAqlVy7do169epJ+A4E4T00Ru7AgQMaQJOQkJCvn9+wYYPG2to62+/5+vpqrKysNE+ePNGmREHIE4O9EZVbUVFRWFlZYW1tna+ft7e3JzExkaSkpLe+169fP8qWLcuMGTO0LVMQcs3oQ/v8+XPs7e3z/fOZP/vixYu3vmdubs7cuXPZtGkTV65cyXcfgpAXRh/aqKgoSUIbFRWV7ff79OmDi4sL06ZNy3cfgpAXhSK0pUuXzvfPvy+0CoWCZcuWceTIEX777bd89yMIuVUoQqvNSGtra0uRIkXeGVoAV1dXunTpwpQpU1AX8AQPofApFKHVZqQFKFWqVI6hBVi6dCm3bt1i61ZxHpCgW0Yf2ujoaOzs7LRqo2TJkkRHR+f4GmdnZ4YMGcLXX3+d7Z1mQZCK0Yc2ISFB6xlLNjY2JOZi3+V58+YRFxfHTz/9pFV/gpATEdpcsLa2JiEh4b2vK126NF988QULFy4kMjJSqz4F4V2MOrRqtZqkpKR8T6zIZGNjk6vQAkyePBkbGxsWLVqkVZ+C8C5GHdqkpCQ0Gk2BjbSZr50+fTpr167lyZMnWvUrCNkx6tBmBk2KkTY3n2kzDR8+nA8++ICFCxdq1a8gZKdQhFaKG1G5HWkhY3rjtGnTWL9+PQ8ePNCqb0H4LxHaXMjL5XGmwYMHU7FiRRYsWKBV34LwX0Yd2uTkZAAsLS21asfKyirPz17NzMyYMWMGGzduJCwsTKv+BeHfjDq0mQvfzczMtGpHqVTmaxF9//79qVKlCt9++61W/QvCvxWK0Jpqud2MqalpvkJramrKV199xbZt23j48KFWNQhCpkIRWqVSqVU7SqUSVT73V+7Xrx/ly5dn6dKlWtUgCJmMOrSZQZMitPndY8rMzIwpU6bg6+vL06dPtapDEMDIQyvl5XF+R1qAoUOHUrJkSX744Qet6hAEKCShlXOkBbCwsGDixImsXr36vUv8BOF9jDq0+nB5nGnU/x0uvWrVKq3aEYRCEVoTE+3epraXx5AxwWPUqFGsXLky6/mxIOSHUYc287OstlvAqFQqrT8XA3z++efEx8ezbds2rdsSCi+jDm3mZXFaWppW7aSnp2t9iQ0Z6229vLxYtmyZ2EtKyLdCEVptP49KFVqAL774gjt37nD48GFJ2hMKH6MObeb0RX0ZaQFq1qxJu3bt+P777yVpTyh8jDq0+jjSAkyaNInjx49z48YNydoUCg+jDq1UI21aWpqkoXV3d6datWqsXr1asjaFwsOoQ/vfkTY1NZXIyMgcl8q9evWK+/fvEx0djeb/TgFNT0/XeqXQvykUCkaMGMGWLVuIi4uTrF2hcDCa82mTk5NZsmQJMTExxMTEEB0dTXh4ODdv3qREiRIkJCSQmpoKQK1atQgJCcm2ndDQUKpXr571ZxsbG0xNTUlNTeWTTz6hVKlS2NnZUaJECerUqUPfvn3zVe+rV68oX74833//PSNHjsxXG0IhJe9Jm9Jq0qSJxsTERGNqaqoBsv3H1NRUM2XKlBzbKVu27Dt/PrMNQPPjjz9qVe/AgQM1tWvX1qoNofAxqsvjCRMmoNFocpy9pFKpcHd3z7GdLl26YG5unmMblpaWDBw4MN+1AowePZqQkBDOnTunVTtC4WJUoe3Ro8d7z+2xsLCgefPmOb7G3d09x5tX5ubmDBkyhGLFiuWrzkyNGjXCxcWFNWvWaNWOULgYVWiVSiVjx459551eExMTWrVqhYWFRY7tuLm55ThfOS0tjXHjxmlVa6YhQ4awZ8+ePG8cJxReRhVayFhNo1Aosv2eqakpnTp1em8bRYsWxcXFJdt2lEolrVu3xtnZWetaIeNQ6vT0dH755RdJ2hOMn9GF1t7eHk9Pz2wf0aSlpb3382ymLl26ZDtiq1QqJk6cqHWdmezs7OjUqRObN2+WrE3BuBldaAEmTpyY7WfScuXK4eTklKs2OnTo8M42OnbsqHWN/zZgwACCg4N5/PixpO0KxskoQ1u/fn1cXFzeWE5nZmZG165dc91GgwYNKF68+BtfUyqVTJo0SZJlev/WuXNn7OzsxIHUQq4YZWghY7T99/K39PT0XF8aQ8ZNKzc3tzcukU1NTRk0aJCUZQIZv1C8vLzEJbKQK0YbWk9PzzdOgDcxMaFNmzZ5aqNjx45ZwTczM2PgwIFanyr/Ln379iU0NJTr16/rpH3BeBhtaM3NzRkzZgxKpRKFQkGDBg3y/Fy1ffv2WfOP09LSGDt2rC5KBTKe2VaoUEHcRRbey2hDCxmPfzQaDRqNhs6dO+f558uXL0+1atUAaNasGbVr15a6xCwKhYJPP/2UwMBAnfUhGAejDm25cuXo3r07QJ4+z/5bly5dgIw1sLr22Wefce3aNf4fe3ceF1W9/w/8NQsMA8POgKICggJakAiGC7hgsiiYKybuZq6JwL1SXrVbuZUtYuWt1NzKMNdMy8xSf26IpRnigteFQBAYNmEY1Jnh/P7gDl9R1pkzc5b5PB+PHo8rMp/P+5w7Lz+fc87nnHPz5k2j90VwF69DC9Q/TM3Z2RkhISF6fT4qKgoeHh4YOXIkzZU9a/DgwXBxccHBgweN3hfBXby5Ne9p1dXVuHr1Ku7fv4/vvvuu3SehdNRqNS5cuID4+Hh07doVvr6+tN4Q/7QZM2YgJycH586dM1ofBLfxKrQlJSXYunUr9u/bhz8uXjTKEw9lNjYYNmwYJiYkYNSoUbTeHA8ABw8exJgxY1BYWAg3Nzda2yb4gRehra6uxqpVq5C2bh2sJVYYN2AgooNDEeTTHV3krhDTsBii9vEj5NzLx/kbV/FD5jn8cvF3dOrkjg8+/BDx8fE0bEW9mpoaODs7Y9OmTZgyZQpt7RL8wfnQpqen4x8pKXikqsXbCdMwKzoWUsuW7+KhQ56iGO/s3I5tvx5BeFgYPv/ii0ZPvDDE0KFD0bFjR7JCimgSZ09EabVapKSkYNKkSXg5OBQ3N36DhSPHmiSwAOAhd8NXSak4//HnqC0pRd/QUBw5coSWtqOionD06FHyQHOiSZwcaWtqavDKhAn49div2JbyJiYM1O8kE10ea9SY8+lH+Pr4L0hLSzP4XtsrV64gMDAQmZmZePHFF2mqkuAL450GNRKtVouJr7yCzLPncOK9NPT178l0SbAUW2Br8pvw7dQZiYmJsLW1NehRNAEBAejSpQt+/vlnElriGZybHqempuLYL8dwcPkqVgT2SUviJ+PN8ZPw2qzXcOLECYPaioyMJK8OIZrEqdDu3r0b69atw5akN9Cvx3NMl9OkldNexci+/RE/frxBL5COjo7GhQsXUFZWRmN1BB9wJrRKpRIpycmYFRWLiYOHMl1Os4QCIbYlL4FEKMSyZcv0bicyMhJCoRC//vorjdURfMCZ0K5evRqqaiVWTZvFdCmtkkmlWDtjLjZv3ozff/9drzbs7OwQGhqKo0eP0lwdwXWcCG1ZWRnS1q3D8lemQG7v0PoHWGDioKHo2+M5vPP223q3MXToUPz222/0FUXwAidCu337dliKxZgdY/xF+3QRCARIGTUeR37+We9nP7300kvIy8vDf//7X5qrI7iME6H9/sABjO4XDhsrK6ZLaZeRoQNga22t9107ffv2hZ2dHRltiUZYH9ra2lpknD+PyKA+TJfSbhZiMSICe+PE8eN6fV4sFiM8PJyElmiE9aG9fv06NBoNgny6M12KXoJ8uuFKM2/oa4uhQ4fi+PHjZEkj0YD1oS0qKgIAdJG3/I4eturkLEdRcbHenx86dCjKy8vx559/0lgVwWWsD21NTQ0AwLqV9++wlUwqhfJ/26CPgIAAdOjQgUyRiQasD63ufobm3s/DBYbckyEQCBAREUFCSzRgfWgJICIiAmfOnMHjx4+ZLoVgARJaDhg4cCBUKhUuXbrEdCkEC5DQckD37t3RsWNHnD59mulSCBYwu9CeuXoFq3Z9/czP8xTFmL/hYwYqapsBAwaQ0BIAzDC0Yc8FoORBBVam/9/LrvIUxZi0dgWSR9P3gDa6hYeH48yZM+R6LWF+oQWA9XMSoaiqxMr0HQ2B3ZL8Jrq7d2a6tGaFh4ejoqIC165dY7oUgmFmGVqgPri5JUUYlJrI+sACQGBgIOzt7ckUmTDf0OYpinEjPw+DAnphz+mTTJfTKpFIhH79+pHQEuYZ2jxFMRLeX4GvklKxLWUJiivLsfo79j9jOCwsDKdOnWK6DIJhZhfaJwPr19kDQP1UmQvBDQ8PR0FBAXJzc5kuhWCQ2YW2ro5qFFid9XMSWX8nUWhoKKysrMgU2cyZXWi93Do8E1idmJBQE1fTPhKJBMHBwSS0Zs7sQst14eHhJLRmjoSWY8LDw5GTk4OSkhKmSyEYwvrQ6l7grNFqGa5EP1ptHa0voR4wYACEQiHOnj1LW5sEt7A+tA4O9Y9MfWDAjeRMqlBWw8Henrb27O3t8fzzz5MpshljfWi9vb0BADcL9HsMKdNuFuTD53/bQBdyXGveWB9aT09PODs54dz1bKZL0UtGzjUEBQfT2mZ4eDguX76M6upqWtsluIH1oRUIBIiOicEPmeeYLqXdCstK8cfNG4iJiaG13bCwMGg0GmRkZNDaLsENrA8tACQkJOB09l+4kZ/HdCnt8tUvP8LJ0RGRkZG0tuvu7g5PT09kZmbS2i7BDZwIbXR0NHy7dcdb32xhupQ2K6uqQtrBfZgzdy6sjPBmhD59+uj9ci+C2zgRWqFQiE83fIY9p0/gZNZlpstpk+Vfb4aFlQSpqalGaZ+E1nxxIrQAMGzYMIwYPhwLv/wEqkcPmS6nRZk517DxyGGs/eAD2NnZGaWPPn36oKioCPfu3TNK+wR7cSa0APDpZ5+hqKoSUz9ajTqKnY9dyVeUYPTK5YiKisSUKVOM1k9ISAiEQiEZbc0Qp0LbtWtX7D9wAIcyM/CvbZuYLucZVaoaxL37Lzh3cEP6rl1GfcC6ra0t/Pz8SGjNEKdCC9Rfo9y0eRM+2LcLC/6TxprljXmKYoSnJqKkphqHDh822rT4SeS41jxxLrQAMHXqVOzZswfbfvsZse8sQYWS2UUGGdevIjR5HiipFTLOn4eXl5dJ+tWFljyh0bxwMrQAMGbMGJw6fRrZhfnwnT0ZX/70A7Qm/vKWVj3AnE8/QvjihQjuG4qzGefg6elpsv779OmDBw8e4NatWybrk2AeZ0MLAMHBwbh67RqmzpyJhV98guBFs7Hjt6NGP7ucpyjGv7/ZAt/XJuPHv/7A9h3bcejwYdja2hq136f16tULlpaWZIpsZgSUIa90Y5EbN27g32+9hQMHvodYJMTAgF4I8u6GLi6uEItEBrdf87AWNwvuIfO/13H51n/hKpdjzty5WLx4MWQyGQ1boJ+QkBCEhYUhLS2NsRoI0+JNaHVKSkpw6NAhnDhxAtlZWbhXUIAHVVXQaDR6t2klkcDJ0QnePt7oHRyMmJgYREREwNLSksbK9TNv3jxkZWWR+2vNCO9C+7Qff/wRsbGxUCqVsLGxaffnt27dioULF0KpVBqhOsNt2bIFCxYsQFVVFSwsLJguhzABTh/TtoVCoYC1tbVegQUAuVyOmpoaqFQqmiujR58+ffDw4UNcvXqV6VIIE+F9aEtKSiCXy/X+vO6zpaWldJVEq549e8LGxoacjDIjvA+tQqGgJbQKhYKukmglEokQFBREQmtGzCK0rq6uen+e7aEFyMooc2MWoTVkpLW1tYWVlRXrQ5udnY3a2lqmSyFMwCxCa8hICwAuLi6sD61Go8Hly9y415gwDO9DW15eDicnJ4PacHZ2Rnl5OU0V0c/Hxwd2dnb466+/mC6FMAHeh1apVBq8Ykkmk6GGxc9dFggEeP7553HlyhWmSyFMgIS2DWxsbFi7uELnhRdeICOtmeB1aOvq6qBSqfReWKEjk8lYH9qAgABkZWWB5wvcCPA8tCqVChRFmc1IW11dTV44bQZ4HVpd0OgYadl8TAvUj7QCgQBZWVlMl0IYmVmElo4TUWwfaW1tbdG1a1dyXGsGSGjbgAvTY6B+ikzOIPMfr0OrWyEklUoNasfa2pq1d/k8KTAwkEyPzQCvQ6u78d3Q+0zFYrFBN9GbSmBgIG7dusWJWQGhP7MIrcjAx82IRCLOhLauro7cW8tzZhFasVhsUDtisRhaljxfuSXe3t6QyWRkisxzvA6tLmh0hJYLI61QKCTLGc0Ar0NL5/SYCyMtQJYzmgOzCK25jLQAWc5oDngdWnObHgP1I21lZSXy8/OZLoUwErMIrVBo2GZyaXocGBhIljPyHK9DqzuWNfQFVVqt1uDjYlOxs7ODp6cnOa7lMV6HVjctVqvVBrWj0WgMnmKbUmBgILKzs5kugzASswitocejXAttz549cePGDabLIIyE16HVLV80t5HWz88POTk55L21PMXr0JrrSOvv74/a2lrk5eUxXQphBLwOLV0jrVqt5lxoAeD69esMV0IYA69D+/RI++jRIxQXFyMnJ6fZz1RWVuLOnTsoLy9vWKCg0Wg49UY6BwcHdOjQgRzX8hR3ho9W1NbWYu3ataioqEBFRQXKy8vx999/QygUIigoCEqlEo8ePQLwf6uGmlJUVIQePXo0/Fkmk0EkEuHRo0cYNGgQXFxc4OTkBEdHR7zwwguYNGmSSbavvfz9/Vv8x4ngLt6EViqV4ujRo8jMzIRAIGi0GKKsrKzhf4tEIkRHRzfbjr+/Pzp27Ij79+8DQKN7U0+dOtXQhlarxfr16+neDNr4+/uT6TFP8Wp6vGjRIlAU1eLqJa1Wi6ioqBbbiY2NbfEt71qtFlKpFNOmTdO7VmPz8/Mj02Oe4lVox44d2+p7eyQSCcLCwlr8naioqBZPXllaWmLmzJmwt7fXq05T8Pf3R3FxMSoqKpguhaAZr0IrFouxYMGCZs/0CoVCDB48GBKJpMV2hg0b1uJ6ZbVajddff92gWo1NdwaZjLb8w6vQAsDcuXMhEAia/DuRSIThw4e32oadnR169+7dZDtisRhDhgxpCAVbeXh4wNramoSWh3gXWrlcjvj4+CYv0ajV6laPZ3ViY2ObHLG1Wi2Sk5MNrtPYhEIhfH19yRlkHuJdaAEgOTm5yWNSd3d3+Pn5tamN6OjoZtuIiYkxuEZT8Pf3JyMtD/EytMHBwejdu3ej2+ksLCwQFxfX5jZCQkLg4ODQ6GdisRgpKSmcuU2PnEHmJ16GFqgfbZ9cMK/RaNo8NQbqp5fDhg1rNEUWiUSYPn06nWUalb+/P27fvo3Hjx8zXQpBI96GNj4+vtEb4IVCISIiItrVRkxMTEPwLSwsMG3aNIPfKm9K/v7+0Gg0uH37NtOlEDTibWgtLS0xf/58iMViCAQChISEtPu6amRkZMP6Y7VajQULFhijVKPx8/ODUCgkU2Se4W1ogfrLPxRFgaIojBgxot2f79SpE3x9fQEAAwYMQGBgIN0lGpVUKoWHhwcJLc/wOrTu7u4YM2YMALTrePZJsbGxAICUlBTa6jIlcgaZf3gdWgBITEyEs7MzQkJC9Pp8VFQUPDw8MHLkSJorMw0fHx/cuXOH6TIIGvHmLp+nVVdX4+rVq1AoFHjppZewefNmvdpRq9UYPHgwjh49iq5du8LX15dTN8R7e3tj//79TJdB0EhA8ehR9CUlJdi6dSv279uHPy5eNMozkmQ2Nhg2bBgmJiRg1KhRrL85/uDBgxg9ejSqq6thY2PDdDkEDXgR2urqaqxatQpp69bBWmKFcQMGIjo4FEE+3dFF7goxDYshah8/Qs69fJy/cRU/ZJ7DLxd/R6dO7vjgww8RHx9Pw1YYR3Z2NgICAnDlyhU8//zzTJdD0IDzoU1PT8c/UlLwSFWLtxOmYVZ0LKSWLd/FQ4c8RTHe2bkd2349gvCwMHz+xReNnnjBFiqVCjKZDN9//z1nj8uJxjh7Ikqr1SIlJQWTJk3Cy8GhuLnxGywcOdYkgQUAD7kbvkpKxfmPP0dtSSn6hobiyJEjJum7PaytreHm5kYWWPAIJ0NbU1ODUS+/jM83/Afpb7yFzxekwNnOjpFa+vj64/TaTzCmbxji4uLw2WefMVJHS8gZZH7hzmnQ/9FqtZj4yivIPHsOJ95LQ1//nkyXBEuxBbYmvwnfTp2RmJgIW1tbVj2KxsfHh4y0PMK50KampuLYL8dwfM06VgT2SUviJ6NaVYvXZr0GDw8PDBkyhOmSANRf9snMzGS6DIImnJoe7969G+vWrcOWpDfQr8dzTJfTpJXTXsXIvv0RP348FAoF0+UAqB9pc3NzOfO6TqJlnAmtUqlESnIyZkXFYuLgoUyX0yyhQIhtyUsgEQqxbNkypssBUD/SPnr0CAUFBUyXQtCAM6FdvXo1VNVKrJo2i+lSWiWTSrF2xlxs3rwZv//+O9PlwMfHBwDIySie4ERoy8rKkLZuHZa/MgVye4fWP8ACEwcNRd8ez+Gdt99muhS4ublBJpORk1E8wYnQbt++HZZiMWbHcGdxgEAgQMqo8Tjy88/Iz89nuhx07dqVjLQ8wYnQfn/gAEb3C4eNlRXTpbTLyNABsLW2xsGDB5kuhVz24RHWh7a2thYZ588jMqgP06W0m4VYjIjA3jhx/DjTpZAFFjzC+tBev34dGo0GQT7dmS5FL0E+3XClmTf0mZK3tzcZaXmC9aEtKioCAHSRt/yOHrbq5CxHUXEx02XAx8cH5eXlqKysZLoUwkCsD21NTQ0AwLqV9++wlUwqhfJ/28Akb29vAOSyDx+wPrS6Owebez8PF7Dh7kcPDw8IBAL8/fffTJdCGIj1oSXoIZFI4OrqyorLT4RhSGjNiIeHBwktD5DQmpEuXbogLy+P6TIIA5ldaM9cvYJVu75+5ud5imLM3/AxAxWZjoeHBwktD5hdaMOeC0DJgwqsTN/R8LM8RTEmrV2B5NHsfUAbHbp06UKmxzxgdqEFgPVzEqGoqsTK9B0Ngd2S/Ca6u3dmujSj8vDwwP3798lb9DjOLEML1Ac3t6QIg1ITzSKwQP1IW1dXh8LCQqZLIQxgtqHNUxTjRn4eBgX0wp7TJ5kuxyQ8PDwAgBzXcpxZhjZPUYyE91fgq6RUbEtZguLKcqz+7humyzI6Nzc3WFpakuNajjO70D4ZWL/O9SPP+jmJZhFcoVCITp06kZGW48wutHV1VKPA6qyfk8jZO4nagyyw4D7OPULVUF5uHZr9u5iQUBNWwgxyrZb7zG6kNXfkWi33kdCaGbKUkftYH1rdC5w1HH3QtlZbx6qXUHt4eKCyshJVVVVMl0LoifWhdXCof2TqAxbcSK6PCmU1HOztmS6jQZcuXQCATJE5jPWh1T1x4WYBN79kNwvy4fO/bWADT09PAGSBBZexPrSenp5wdnLCuevZTJeil4ycawgKDma6jAZ2dnawtbXFvXv3mC6F0BPrQysQCBAdE4MfMs8xXUq7FZaV4o+bNxATE8N0KY24u7vj/v37TJdB6In1oQWAhIQEnM7+CzfyuTWl++qXH+Hk6IjIyEimS2mkY8eOJLQcxonQRkdHw7dbd7z1zRamS2mzsqoqpB3chzlz58KKZW9GIKHlNk6EVigU4tMNn2HP6RM4mXWZ6XLaZPnXm2FhJUFqairTpTyjY8eO5PY8DuNEaAFg2LBhGDF8OBZ++QlUjx4yXU6LMnOuYeORw1j7wQews7NjupxnkJGW2zgTWgD49LPPUFRViakfrUYdVcd0OU3KV5Rg9MrliIqKxJQpU5gup0nu7u4oLi5GXR079yHRMk6FtmvXrth/4AAOZWbgX9s2MV3OM6pUNYh7919w7uCG9F27WPuA9Y4dO0KtVqOsrIzpUgg9cCq0ABAeHo5Nmzfhg327sOA/aaxZ3pinKEZ4aiJKaqpx6PBhVk6LdTp27AgA5LiWozgXWgCYOnUq9uzZg22//YzYd5agQlnNaD0Z168iNHkeKKkVMs6fh5eXF6P1tEYXWnJcy02cDC0AjBkzBqdOn0Z2YT58Z0/Glz/9AK2Jj9FKqx5gzqcfIXzxQgT3DcXZjHMNywTZzN7eHjY2NiS0HMXZ0AJAcHAwrl67hqkzZ2LhF58geNFs7PjtqNHPLucpivHvb7bA97XJ+PGvP7B9x3YcOnwYtra2Ru2XTh06dCCh5SgBxYZXutHgxo0b+Pdbb+HAge8hFgkxMKAXgry7oYuLK8QikcHt1zysxc2Ce8j873VcvvVfuMrlmDN3LhYvXgyZTEbDFphWeHg4evXqhU8//ZTpUoh24k1odUpKSnDo0CGcOHEC2VlZuFdQgAdVVdBoNHq3aSWRwMnRCd4+3ugdHIyYmBhERETA0tKSxspNa8KECdBqtdi7dy/TpRDtxLvQPu3HH39EbGwslEolbGxs2v35rVu3YuHChVAqlUaojjlJSUm4cOECzp3j3o0Y5o7Tx7RtoVAoYG1trVdgAUAul6OmpgYqlYrmyphFVkVxF+9DW1JSArlcrvfndZ8tLS2lqyRW0IWW5xMtXuJ9aBUKBS2hVSgUdJXECh07dsSjR49QWVnJdClEO5lFaF0BTRjKAAAgAElEQVRdXfX+PJ9DC5BVUVxkFqE1ZKS1tbWFlZUV70Lr5uYGoP7wgeAWswitISMtALi4uPAutE5OThCJRLzbLnPA+9CWl5fDycnJoDacnZ1RXl5OU0XsIBKJ4OjoSELLQbwPrVKpNHjFkkwmQw1Hn7vcErlcTkLLQSS0bWBjY8O7xRUACS1X8Tq0dXV1UKlUei+s0JHJZCS0BGvwOrQqlQoURZGRthmurq4ktBzE69DqgkbHSEuOaQm2MIvQ0nEiio8jLQktN5HQtgFfp8dyuRxlZWXkqYwcw+vQ1tbWAgCkUqlB7VhbW/PuLh+gPrRarRYVFRVMl0K0A69Dq7vx3cLCwqB2xGKxQTfRsxVf11XznVmEVmTg42ZEIhEJLcEaZhFasVhsUDtisRhaljxfmU4uLi4QCAQktBzD69DqgkZHaPk40orFYjg4OJDQcgyvQ0vn9JiPIy1ALvtwkVmEloy0zSOh5R5eh5ZMj1tHQss9ZhFaodCwzeTz9NjZ2Zm8PY9jeB1a3bGsoSt+tFqtwcfFbOXk5EQWV3AMr0Ormxar1WqD2tFoNAZPsdnK0dGRhJZjzCK0hh6P8j20fHuUDt/xOrS65YtkpG2ek5MTHjx4QG4a4BBeh5aMtK1zdHSEVqtFVVUV06UQbcTr0NI10qrVal6HFgA5ruUQXof26ZH20aNHKC4uRk5OTrOfqaysxJ07d1BeXt7wnhuNRmPwnUJspXu8LDmu5Q7eDB+1tbVYu3YtKioqUFFRgfLycvz9998QCoUICgqCUqnEo0ePAAABAQHIyspqsp2ioiL06NGj4c8ymQwikQiPHj3CoEGD4OLiAicnJzg6OuKFF17ApEmTTLJ9xkJGWu7hTWilUimOHj2KzMxMCASCRoshnlw8IBKJEB0d3Ww7/v7+jV4D+eQTK06dOtXQhlarxfr16+neDJOzt7eHSCQiIy2H8Gp6vGjRIlAU1eLqJa1Wi6ioqBbbiY2NbfEt71qtFlKpFNOmTdO7VrYQCoWws7MjIy2H8Cq0Y8eObfW9PRKJBGFhYS3+TlRUVIsnrywtLTFz5kzY29vrVSfbkFVR3MKr0IrFYixYsKDZM71CoRCDBw+GRCJpsZ1hw4a1uF5ZrVbj9ddfN6hWNiGroriFV6EFgLlz50IgEDT5dyKRCMOHD2+1DTs7O/Tu3bvJdsRiMYYMGQJ/f3+Da2ULJycnckzLIbwLrVwuR3x8fJOXaNRqdavHszqxsbFNjtharRbJyckG18kmZKTlFt6FFgCSk5ObPCZ1d3eHn59fm9qIjo5uto2YmBiDa2QTMtJyCy9DGxwcjN69eze6nc7CwgJxcXFtbiMkJAQODg6NfiYWi5GSksK72/TISMstvAwtUD/aPrkIXqPRtHlqDNSftBo2bFijKbJIJML06dPpLJMVHBwcUFlZyXQZRBvxNrTx8fGN3gAvFAoRERHRrjZiYmIagm9hYYFp06YZ/FZ5NrKzsyM3DHAIb0NraWmJ+fPnQywWQyAQICQkpN3XVSMjIxvWH6vVaixYsMAYpTKOhJZbeBtaoP7yD0VRoCgKI0aMaPfnO3XqBF9fXwDAgAEDEBgYSHeJrGBnZweNRsPL9xXxEa9D6+7ujjFjxgBAu45nnxQbGwsASElJoa0utrGzswMAMtpyBK9DCwCJiYlwdnZGSEiIXp+PioqCh4cHRo4cSXNl7EFCyy28ucvnadXV1bh69SoUCgVeeuklbN68Wa921Go1Bg8ejKNHj6Jr167w9fXl3Q3xutBWV1czXAnRFgJKd6aFB0pKSrB161bs37cPf1y8aJTnHslsbDBs2DBMTEjAqFGjeHFzfGlpKeRyOY4fP44hQ4YwXQ7RCl4MGdXV1Vi1ahXS1q2DtcQK4wYMxBtL3kGQT3d0kbtCTMNiiNrHj5BzLx/nb1zFD5nnMPGViejUyR0ffPgh4uPjadgK5pDpMbdwfqRNT0/HP1JS8EhVi7cTpmFWdCykli3fxUOHPEUx3tm5Hdt+PYLwsDB8/sUXjZ54wTVSqRQbN27ElClTmC6FaAVnT0RptVqkpKRg0qRJeDk4FDc3foOFI8eaJLAA4CF3w1dJqTj/8eeoLSlF39BQHDlyxCR9GwO5VssdnAxtTU0NRr38Mj7f8B+kv/EWPl+QAuf/TfFMrY+vP06v/QRj+oYhLi4On332GSN1GIqEljs4d0yr1Wox8ZVXkHn2HE68l4a+/j2ZLgmWYgtsTX4Tvp06IzExEba2tpx7FI2dnR05e8wRnAttamoqjv1yDMfXrGNFYJ+0JH4yqlW1eG3Wa/Dw8ODUmVgy0nIHp6bHu3fvxrp167Al6Q306/Ec0+U0aeW0VzGyb3/Ejx/Pqfe+ktByB2dCq1QqkZKcjFlRsZg4eCjT5TRLKBBiW/ISSIRCLFu2jOly2oyEljs4E9rVq1dDVa3EqmmzmC6lVTKpFGtnzMXmzZvx+++/M11Om5DQcgcnQltWVoa0deuw/JUpkNs7tP4BFpg4aCj69ngO77z9NtOltImtrS05EcURnAjt9u3bYSkWY3YMdxbtCwQCpIwajyM//4z8/Hymy2mVtbU1ampqmC6DaANOhPb7Awcwul84bKysmC6lXUaGDoCttTUOHjzIdCmtsrGxIaHlCNaHtra2FhnnzyMyqA/TpbSbhViMiMDeOHH8ONOltIqEljtYH9rr169Do9EgyKc706XoJcinG64084Y+NiGh5Q7Wh7aoqAgA0EXe8jt62KqTsxxFxcVMl9EqGxsbPHz4sMWXlxHswPrQ6v71t27l/TtsJZNKoeTACGZtbQ0A5DlRHMD60OruHGzu/TxcwIW7H21sbACATJE5gPWhJUyDhJY7SGgJACS0XEJCSwAgoeUSswvtmatXsGrX18/8PE9RjPkbPmagInbQnYgioWU/swtt2HMBKHlQgZXpOxp+lqcoxqS1K5A8mtsPaDOEbqQlZ4/Zz+xCCwDr5yRCUVWJlek7GgK7JflNdHfvzHRpjJFIJBCLxWSk5QCzDC1QH9zckiIMSk00+8DqkJsGuMFsQ5unKMaN/DwMCuiFPadPMl0OK5CljNxglqHNUxQj4f0V+CopFdtSlqC4shyrv/uG6bIYR0LLDWYX2icD69fZA0D9VJkEF7CyssKjR4+YLoNohdmFtq6OahRYnfVzEjl7JxFdJBIJHj58yHQZRCs49whVQ3m5dWj272JCQk1YCftYWVmR0HKA2Y20RPNIaLmBhJZoQELLDawPre4FzhqO3pyt1dZx5iXU5EQUN7A+tA4O9Y9MfcDRSxEVymo42NszXUabkJGWG1gfWm9vbwDAzQL2P4a0KTcL8uHzv21gOxJabmB9aD09PeHs5IRz17OZLkUvGTnXEBQczHQZbUJCyw2sD61AIEB0TAx+yDzHdCntVlhWij9u3kBMTAzTpbQJCS03sD60AJCQkIDT2X/hRn4e06W0y1e//AgnR0dERkYyXUqbkMUV3MCJ0EZHR8O3W3e89c0Wpktps7KqKqQd3Ic5c+fCiiNvRiCh5QZOhFYoFOLTDZ9hz+kTOJl1mely2mT515thYSVBamoq06W0GZkecwMnQgsAw4YNw4jhw7Hwy0+gesTuL1ZmzjVsPHIYaz/4AHZ2dkyX02YktNzAmdACwKeffYaiqkpM/Wg16qg6pstpUr6iBKNXLkdUVCSmTJnCdDntIpVKSWg5gFOh7dq1K/YfOIBDmRn417ZNTJfzjCpVDeLe/RecO7ghfdcuzj1g3cLCAmq1mukyiFZwKrQAEB4ejk2bN+GDfbuw4D9prFnemKcoRnhqIkpqqnHo8GFOTYt1SGi5gXOhBYCpU6diz5492Pbbz4h9ZwkqlMy+wTzj+lWEJs8DJbVCxvnz8PLyYrQefelCy4XXmJgzToYWAMaMGYNTp08juzAfvrMn48uffoC2zrTHuaVVDzDn048QvnghgvuG4mzGOXh6epq0BjpZWloCADQaDcOVEC3hbGgBIDg4GFevXcPUmTOx8ItPELxoNnb8dtToZ5fzFMX49zdb4PvaZPz41x/YvmM7Dh0+DFtbW6P2a2wWFhYAQKbILCegeDIXunHjBv791ls4cOB7iEVCDAzohSDvbuji4gqxSGRw+zUPa3Gz4B4y/3sdl2/9F65yOebMnYvFixdDJpPRsAXM++WXXxAVFYWKioqGu6sI9uFNaHVKSkpw6NAhnDhxAhcyM1FaWgplTY1Bo4eNtTXs7ezh7eON3sHBiImJQURERMN08u7duzhz5gznLvE87eTJkxgyZAhKSkogl8uZLodoDsVjEyZMoF5++WW9Pz9kyBBq/vz5rf7e+vXrKbFYTGVkZOjdFxucOXOGAkAVFBQwXQrRAk4f07bm1q1b6Natm96ft7S0xOPHj1v9vYULFyIqKgqTJk1CVVWV3v0xTXdM25ZtJpjD69DeuXMHPj4+en/e0tKyTY9fEQgE2LJlC1QqFRITE/Xuj2nkRBQ38Da0paWlqKioMGiklUgkbR51XF1dsXXrVuzYsQPp6el698kk3TE6CS278Ta0t2/fBgCTTI91oqOjMX/+fMybNw+5ubl698sUMtJyA29De+vWLVhaWsLDw6P1X25GW6fHT/rwww/h6emJKVOmQMuSJZZtRY5puYHXoe3atStEBlyjbe9IC9Tf3vbtt9/i4sWLWLNmjd59M4FMj7mBt6HNzc1F165dDWpD3wX0zz33HFavXo133nkHGRkZBtVgSkJh/deBazMEc8Pb0BYUFKBzZ8NeFK3Vahu+yO21aNEiREVFYfLkyZy5DKTb1joTr+Em2ofXoe3UqZNBbWi1Wr2n11y8DKS7/5fi1yI53uFtaAsLC+Hu7m5QG4aEFuDeZSAy0nIDL0OrUqlQWVnJeGgBbl0GIqHlBl6GtqCgAABYEVqAO5eBSGi5gZehLSwsBABGj2mfxJXLQLrQkmNaduNlaAsKCiAWiw2+vYyu0ALcuAykOxFFRlp242Voi4uL4ebmpvflGh06Qwuw/zIQmR5zAy9DW1ZWBhcXF4PbqampgY2NDQ0V1WP7ZSASWm7gZWjLy8vh5ORkcDt0hxZg92UgckzLDbwMbVlZGZydnQ1up6amxijPf2LrZSByTMsNvAwtXSOtUqmkfaTVYeNlIDI95gYS2hYYY3qsw8bLQCS03MDL0JaVldEWWmM+HpVtl4HIMS038DK05eXltB3TGmuk1WHbZSCBQEBGWpbjXWg1Gg2USiUcHR0Naufx48dQq9VGDy3bLgMJhUISWpbjXWiVSiUoijJ4WltZWQkABoe/Ldh0GYiElv14F1qVSgUAsLa2NqidsrIyAKDl2Lgt2HIZiISW/Uhom6ELLR3Hxm3FhstAAoGAnIhiORLaZph6pAXYcRmIjLTsx7vQ1tTUAKAntDKZDBKJhI6y2ozpy0AktOzHu9DSOdKacmr8JCYvA5HQsp+Y6QLopk9oc3JycOPGDXh5ecHT0xMODg6MhlZ3GeiFF15AYmIitm3b9szvaDQaiMWG/d93/vx5KJVKAICDg0PDNdrc3FxcvHix0e/6+fnx5j28XMfp99Pm5uZizJgxUKvVePjwIWpra/Hw4UNUVFSgY8eODb8nEAjQq1cvHDp0qMl2zp49i7CwsIY/y2QyiMViWFhYICEhAV5eXg2B7tq1q8leuPzzzz9j+PDh2LlzJyZOnNjw89u3b2PSpEnYsWMHfH199W5/9uzZ2LRpU6u/JxAIcPfuXXh6eurdF0Ej5t6ySY/u3btTAFr9Ly0trdk2VCoVJRKJmvycpaUlZWlp2fDnTz75xIRbR1ELFiyg7O3tqbt371IURVHbtm2jrK2tKQDUxx9/bFDbv/76a6v7TSgUUv369aNhSwi6cD60q1atosRicYtfPIFAQN27d6/FdgICAlr9Asvlcqq2ttZEW1avtraWCgwMpPr27UuNGzeuYXuEQiE1ePBgg9rWaDSUk5NTi9ssEomojRs30rQ1BB04H9q///6bEggELY4UAwYMaLWd+fPnNxpRm/ryrl+/3gRb9Kyvv/6aEggEz8wGxGIxVVVVZVDb8+fPpywsLJrdbrFYTJWVldG0JQQdOH/22MPDA/369Wv2eVACgQCTJk1qtZ3Q0FBoNJpm/97BwQGvvfaa3nXqQ6PR4O2338a0adMgFAqfWXCh1Wrx22+/GdTHhAkTmn1fkVgsRmxsrEmvVROt43xoAWDatGkNT114GkVRGDt2bKtt9O3bt9lLHWKxGEuXLoVUKjWozva4ffs2+vbti5UrV6Kurq7JFVJisRg//fSTQf2EhYXB1dW1yb/TarWYOnWqQe0TRsD0UE+HysrKJqd4IpGIGjJkSJvaqKuro+zs7JqcIjo5OVE1NTVG3or/8/jxY6p///5tOsEml8upuro6g/pLSkpqcv/Z2tpSDx8+pGmrCLrwYqS1t7fHiBEjmrxu2ZapMVA/jQ4NDX1mxBaLxfjXv/5l8GKN9rCwsMBvv/2GN954AwKBoMXHuCoUCvz1118G9dfUFNnCwgITJ040+YowonW8CC0ATJ06tckp5KhRo9rcRv/+/Rvehq4jk8kwZ84cg+trLysrK7z33ns4duwYXFxcnqlLx9LS0uApcmho6DOvUFGr1Zg8ebJB7RLGwZvQjhgxAra2tg1/FolEeOmll9q1qqlv376N3vyuG2WZXAk0dOhQ3LhxA2PGjAGAZ2YCarUa33//vUF9CAQCTJ48udE/DO7u7o0WnBDswZvQWlpaYuLEiQ1fPIqikJCQ0K42+vbt2ygUNjY2mDt3Lq116sPBwQG7du3C7t27YWNj0yhcFEXh4sWLDXcl6evJKbKFhQVmzJjR7Mk9gmFMH1TT6dSpUw0nUSwsLKjKysp2t+Hl5dVwffK9994zQpWGyc3Npfr169fomq1QKKR27txpcNuenp4NbV69epWGaglj4M1IC9RfvtC9KS8qKgr29vbtbmPgwIEA6m84mD9/Pq310cHT0xOnT5/Gv//9b4hEIojFYggEAhw+fNjgtqdMmQIACAwMRM+ePQ1ujzAOXoVWIBBg+vTpANDuqbFOaGgoAOCNN95odIzMJiKRCMuXL0dGRga6dOkCrVaLH3/80eCnXcTHxwNAwz4kWIrpoZ5u165do6ytranq6mq9Pv/HH39QTk5O1IMHD2iuzDiUSiX16quvUgCoM2fOGNxeQEBAq+u0CWZx+tY8nYKCAuTk5KCsrAwVFRU4evQooqKi9GrLzs4Op06dwrJlywx+k7wp/fzzz7h16xZef/31Z/7u6f3Tktu3b8PHx6fZv3d0dISzszP8/f05tX94hel/NfT1xx9/UPPnz6c8Ondp08ohff7z6NyFWrBgAXXx4kWmN7dN1Gp1w/8m+4e/ODfSXrt2DclJSfjl2DE85+WNsf3DMSSwN5736goXu/afeGpKadUDZOfexYmsS9h37jSu5t5B5LBhWJeWxvoTNGT/8B9nQqtSqbB06VJs+GwDAr19sHbGHES80NskfR//6xJSt3yJrLu3sOD117Fq1SqTLmtsC7J/zAcnQltYWIiXR47E3Vu3sGbabLwaNRxCgWlPfNdRdfjq6E9Ysn0junbrhoM//MCaYzqyf8wL60N7+fJlxMXGwlZsgUNvrYFPR2a/CLfvFyLu3SWoVqtx6MfD6NWrF6P1kP1jflgd2tzcXIS++CKe7+SBfUvfhYMNO54GWFmjxNhVbyG7IA+ZFy7Ay8uLkTrI/jFPrA1tdXU1BvTvD9HDxzj9/ieQmfAG9LZQPXqIwW8mQSmgcC4jw2RPaNQh+8d8sXZF1MwZM1B6vwiH3lrNui8kAFhLrLB/6QpUKkrx2qxZJu+f7B/zxcrQHjt2DHv37cP2lCXo7CJnupxmdXaRY3vKEuzdtw/Hjh0zWb9k/5g31k2PNRoNgnr1QncHZ+xfuoLpctpk9MplyClX4K+srGZvVqcL2T8E60banTt34ubNm/jw1XlMl9JmH82aj9u3b+Pbb781el9k/xCsG2n79+0HL6kM36YuZ7qUdpn4/rvIe6zC2XPnjNoP2T8Eq0ba+/fv4/yFTEwa/BLTpbTb5CHDkHH+PO7fv2+0Psj+IQCWhfbkyZMQi0SI6GWa5Xd0iujVG2KRCCdPnjRaH2T/EADLQpuVlQV/Dy9ILbn32E6ppQT+Hl7Izs42Wh9k/xAAy0JbVFSEzs4uTJeht07OzigqKjJa+2T/EADLQltTUwMbiRXTZehNJpGiurraaO2T/UMALAstAHD5qZ2mqJ3sH4J1oSUIomUktATBMSS0BMExvA7tmatXsGrX18/8PE9RjPkbPmagInYh+4ebeB3asOcCUPKgAivTdzT8LE9RjElrVyB5dDyDlbED2T/cxOvQAsD6OYlQVFViZfqOhi/kluQ30d29M9OlsQLZP9zD+9AC9V/M3JIiDEpNJF/IJpD9wy1mEdo8RTFu5OdhUEAv7Dl9kulyWIfsH27hfWjzFMVIeH8FvkpKxbaUJSiuLMfq775huizWIPuHe3gd2ie/kH6dPQDUTwXJF7Me2T/cxOvQ1tVRjb6QOuvnJCLIpztDVbEH2T/cJGa6AGPycuvQ7N/FhISasBJ2IvuHm3g90hIEH5HQEgTHsCq0IpEIGm0d02XoTaOtg1hsvCMOsn8IgGWhdXBwQKVKyXQZeqtUKY36+guyfwiAZaH19vbGzYJ7TJeht5x7+fDx8TFa+2T/EADLQtu7d28UKEqQpyhmupR2y1MUo7BUgaCgIKP1QfYPAbAstGFhYbCVyfDD+bNMl9JuBzPOwFYmw4ABA4zWB9k/BMCy0EokEowbPx6bjv7IdCnttvmXnzBu/HhIJMZ7vCnZPwTAstACwMKFC5Gde4dTC9f3nD6J7Nw7WLhwodH7IvuHYN27fABg5syZ+PXHn3D9ix2wsWL3I0NrHz9Cz7nTEDE8Bl999ZVJ+iT7x7yxbqQFgDVr1uBBrQor0rczXUqr3tm5DeU1SqxevdpkfZL9Y95YGVo3NzesS0vD2r3p+Pbkr0yX06xvT/6KtXvTsS4tDW5ubibrl+wf88ba5SkzZ87EtWvX8GraWni6umFAzwCmS2rk7LUreDVtLVJSUjBz5kyT90/2j/li5TGtTl1dHcaOGYNjv/yCnYuX4eW+YUyXBAA4eP4MJn2wEsMiI7Fv/34IhcxMWMj+MU+it99++22mi2iOQCDAuHHjcK+gAG+kfQippQT9ezwPAUPvl6AoCh/s24XZn3yIGTNnYtu2bRCJRIzUApD9Y65YPdI+KS0tDYsXL0bvbr74dE4iXvTrYdL+L+Rcx8IvP8GlWzfxwQcfICkpyaT9t4bsH/PBmdACwJUrV7AoMREn/9//w4SBEVgQOxoDehpvZKEoCmevZWPD4QP47tRxDB40COs/+QQBAew6ftQh+8c8cCq0Onv37sV7a9bg4qVL6OjsgsEBvfCchxfk9vTcQVJSWYFr+X/j5JXLuF9WipDgYLzx5psYN24cLe0bG9k//MbJ0OpkZWXh0KFDyDh3DjdzclBUXIxqpWG3rtnKZHBzdYWfvz/69e+PuLg4BAYG0lSxaT29fxSlpah88MCgNh3s7eHi7MyL/cNVnA7t06ZPn46SkhL89NNPen0+JiYGHTp0wNatW2mujBvi4+tfBbJ7926GKyFawqtz8QqFAq6urnp/Xi6Xo7S0lMaKCIJ+vAutXC7X+/NyuRwKhYLGigiCfrwKbUlJCQktwXu8Ci0ZaQlzwJvQqlQqqFQqg49pq6ur8fDhQxorIwh68Sa0JSUlAGDwSAuAnIwiWI03oa2oqAAAODk56d2G7rPl5eW01EQQxsCb0FZXVwMAbG1t9W5DJpMBAJQGLtAgCGPiTWh1QdMFTx8ktAQX8Cq0AoEAUqlU7zZIaAku4E1oa2pqYG1tbdAN1yKRCFZWViS0BKvxJrRKpdKgqbGOTCYjoSVYjYT2KTY2NiS0BKvxJrQ1NTWwsbExuB2ZTIaamhoaKiII4+BNaFUqFaytrQ1ux8bGhoSWYDXehFaj0dDywmKRSAStVktDRQRhHLwJrVarpSW0YrEYGo2GhooIwjh4E1q6RlqxWExGWoLVeBVaOp6xKxKJyEhLsBpvQkvn9JiMtASb8Sa0dE6PyUhLsBmvQkumx4Q54E1o6+rqaAktmR4TbMeb0AqFQtTV1RncjlarJS+NIliNN6EVi8VQq9UGt0PXsTFBGAtvQmthYUHLsSgJLcF2vAktGWkJc8Gr0NIx0qrVahJagtV4E1oyPSbMBW++nU9Pj5VKJSoqKiAQCNC5c+cmP3Pv3j1QFAVHR8eGG+g1Gg0sLCxMUjNB6IOTof3zzz+xb98+VFRUoKKiAgqFAtnZ2aisrISTkxOqqqoarrWuWLECy5Yta7Kdbdu2Yfny5QDqF1XY2dmhtrYWf//9N/766y/I5XI4OjrC0dERY8eORVBQkMm2kSCaw8n30+bn56Nr164A6hdVtLQJmZmZePHFF5v8uwsXLiA0NLTZzwoEgoYHxd29exddunQxoGr2I++n5QZOHtN26dIFI0eOhFAobDGwdnZ2CAkJafbvQ0JC4ODg0OzfUxQFoVCIkSNH8j6wBHdwMrQAsGjRohYv8YjFYkRHR7f4SFWhUIjIyMgWTzyp1WokJSUZVCtB0ImzoR00aBB69OjRbCgpikJMTEyr7URHRze7/FEgEMDPzw/h4eEG1UoQdOJsaAEgOTm52b+rq6tDZGRkq21ER0c3O8UWCoX45z//CYFAoHeNBEE3Tod28uTJzT7r2M/PD+7u7q220bFjR/j6+jb5d9bW1pg4caJBNRIE3TgdWqlUitmzZz9zXdXS0hJxcXFtbicuLg6WlpaNfmZhYYG5c+fS8ixlgqATp0MLAK+//voz978+fvwYUVFRbW4jKioKjx8/bvQzjUaDefPm0VIjQdCJ86H19PRETExMo9FWIl71tpgAACAASURBVJEgLCyszW2Eh4fDysqq4c9isRgjRoxouBZMEGzC+dACQFJSUsPlH6FQiMGDB0MikbT58xKJBAMHDmw4E63RaMhlHoK1eBHal156CX5+fhAIBBCJRBg+fHi72xg+fDiEQiEEAgG6deuGiIgII1RKEIbjRWgBIDExEUD9Yoi2XOp5WlRUVMNdQsnJyeQyD8FanFx73BSlUokOHTrAzs4OhYWFerXh7u6OqqoqFBUV0fLaTK4ha4+5gZN3+TRFJpNh1qxZqK2t1buNuLg4SKVSswwswR28CG1BQQFycnLg4+OD69evY+PGjXq1IxQK4e3tjePHj8Pf379NizMIwtQ4G9qLFy9iy5YtOPzDIeTdyzdKHx6duyDu5ZGYOXMmevfubZQ+CKK9OBfaa9euITkpCb8cO4bnvLwxfWAEhgT2xvNeXeFiZ09LH6VVD5Cdexcnsi5h349HsGHDBkQOG4Z1aWno2bMnLX0QhL44cyJKpVJh6dKl2PDZBgR6+2DtjDmIeME0o9/xvy4hdcuXyLp7Cwtefx2rVq2i5a3zbENORHEDJ0bawsJCvDxyJO7euoUN85PwatRwCAWmu1oV8UJvXEj7HF8d/QlLtm7EmdOncfCHH8gxL8EI1l+nvXz5MkJffBE1pWXI/PgLvBYda9LA6ggFQrwWHYvMj79ATWkZQvu8iMuXL5u8DoJgdWhzc3MRFRkJX3kHnPtwA3w6Mj+y+XR0x7kPN8DXtQOiIiORm5vLdEmEmWFtaKurqzEyLg7u9o44uHwVHGzYc+3UwUaGQ/9eDU8nFwyPiUFlZSXTJRFmhLWhnTljBkrvF+HQW6shk0qZLucZ1hIr7F+6ApWKUrw2axbT5RBmhJWhPXbsGPbu24ftKUvQ2UXOdDnN6uwix/aUJdi7bx+OHTvGdDmEmWBdaDUaDVKSkzF6wEAMC2r+8adsMSwoBKP6h2NRYiItLwAjiNawLrQ7d+7EzZs38eGr3HlqxEez5uP27dv49ttvmS6FMAOsC+2Xn3+BsQMGwbsD82eK28q7gzvG9B+IjV9+yXQphBlgVWjv37+P8xcyMWnwS0yX0m6ThwxDxvnzuH//PtOlEDzHqtCePHkSYpEIEb24tzg/oldviEUinDx5kulSCJ5jVWizsrLg7+EFqWXbn+/EFlJLCfw9vJCdnc10KQTPsSq0RUVF6OzswnQZeuvk7IyioiKmyyB4jlWhrampgY3EqvVfZCmZRIrq6mqmyyB4jlWhBQAuP0+Ny7UT3MG60BIE0TISWoLgGBJaguAYXof2zNUrWLXr62d+nqcoxvwNHzNQEUEYjtehDXsuACUPKrAyfUfDz/IUxZi0dgWSR8czWBlB6I/XoQWA9XMSoaiqxMr0HQ2B3ZL8Jrq7d2a6NILQC+9DC9QHN7ekCINSE0lgCc4zi9DmKYpxIz8PgwJ6Yc/pk0yXQxAG4X1o8xTFSHh/Bb5KSsW2lCUorizH6u++YbosgtAbr0P7ZGD9OnsAqJ8qk+ASXMbr0NbVUY0Cq7N+TiKCfLozVBVBGIYTbxjQl5dbh2b/LiYk1ISVEAR9eD3SEgQfkdASBMewKrQikQgabR3TZehNo62DWMzrIw6CBVgVWgcHB1SqlEyXobdKlRIODg5Ml0HwHKtC6+3tjZsF95guQ2859/Lh4+PDdBkEz7EqtL1790aBogR5imKmS2m3PEUxCksVCAoKYroUgudYFdqwsDDYymT44fxZpktpt4MZZ2Ark2HAgAFMl0LwHKtCK5FIMG78eGw6+iPTpbTb5l9+wrjx4yGRcO/xrwS3sCq0ALBw4UJk597h1ML+PadPIjv3DhYuXMh0KYQZYF1og4KCMG3aNPzjq/+g5uFDpstpVe3jR0jd+gWmT59OjmcJk2BdaAFgzZo1eFCrwor07UyX0qp3dm5DeY0Sq1evZroUwkywMrRubm5Yl5aGtXvT8e3JX5kup1nfnvwVa/emY11aGtzc3JguhzATrF2+M3PmTFy7dg2vpq2Fp6sbBvQMYLqkRs5eu4JX09YiJSUFM2fOZLocwoywcqTVWbt2LaJjohG1fDEOnj/DdDkNDp4/g6jlixEdE421a9cyXQ5hZlgdWqFQiD1792Ly1KkYs3I51u5NB0VRjNVDURTW7k3HmJXLMXnqVOzZuxdCIat3IcFDrJ0e64jFYnzxxRfw9/fH4sWLse/cKXw6JxEv+vUwaR0Xcq5j4Zef4NKtm/joo4+QlJRk0v4JQoczw0RSUhIuXboEmw6u6JsyDxPffxdnrl4x6shLURTOXL2Cie+/i74p82DTwRWXLl0igSUYxfqR9kkBAQE4fuIE9u7di/fWrEH44tfR0dkFgwN64TkPL8jt6bnDpqSyAtfy/8bJK5dxv6wUIcHB2L17N8aNG0dL+wRhCAHF5EGigbKysnDo0CFknDuHmzk5KCouRrXSsFv7bGUyuLm6ws/fH/3690dcXBwCAwMb/c6dO3dw6tQpTJ8+3aC+2CY+vv6tC7t372a4EqIlnBppnxYYGNgoUNOnT0dJSQl++uknvdqLiYlBhw4dsHXr1hZ/78iRI1i0aBG6dOmCoUOH6tUXQeiLM8e0baFQKODq6qr35+VyOUpLS1v9vQULFiAhIQEJCQm4d4+79/8S3MS70Mrlcr0/L5fLoVAo2vS7//nPf+Di4oJx48bh8ePHevdJEO3Fq9CWlJSYLLQymQwHDhzA9evXkZqaqnefBNFevAqtKUdaAPD19cXGjRuxfv16fPMNeWMBYRq8Ca1KpYJKpTL4mLa6uhoP23FL4IQJE7Bw4ULMmzcP165d07tvgmgr3oS2pKQEAAweaQG06WTUkz766CP06tULY8aMQVVVld79E0Rb8Ca0FRUVAAAnJye929B9try8vF2fs7CwQHp6OsrLyzF79my9+yeItuBNaKurqwEAtra2erchk8kAAEo9Fmh07twZu3btwt69e/HZZ5/pXQNBtIY3odUFTRc8fRgSWgCIiIjA22+/jZSUFJw9y70nShLcwKvQCgQCSKVSvdswNLQAsHTpUowYMQKvvPJKu85EE0Rb8Sa0NTU1sLa2Nuj+VpFIBCsrK4NCKxAIsGXLFlhaWuKVV16BVqvVuy2CaApvQqtUKg2aGuvIZDKDQgsAjo6O2L9/PzIyMvDuu+8aXBNBPImE9ik2NjYGhxYAXnjhBaxbtw4rV67EkSNHDG6PIHR4E9qamhrY2NgY3I5MJkNNTQ0NFQFz5szB1KlTMWnSJNy9e5eWNgmCN6FVqVSwtrY2uB0bGxvaQgsAn3/+Oby8vDBmzBjU1tbS1i5hvngTWo1GQ8sLnUUiEa0nj6ysrLB7927cvXsXKSkptLVLmC/ehFar1dISWrFYDI1GQ0NF/6dbt27YsWMHvvzyS2zbto3Wtgnzw5vQ0jXSisVio1ymGTlyJP7xj39g3rx5+PPPP2lvnzAfvAqtSCQyuB2RSET7SKuzZs0ahIaGIj4+Hg8ePDBKHwT/8Sa0dE6PjbUgQiwW47vvvoNKpcLUqVMZffA6wV28CS2d02NjjbRA/cvF9uzZgyNHjuDjjz82Wj8Ef/EqtGyfHuv0798fq1atwptvvolTp04ZtS+Cf3gT2rq6OlpCa8zp8ZP++c9/4uWXX0Z8fDwKCwuN3h/BH7wJrVAoRF1dncHtaLVaWsLfGoFAgK1bt8LR0REJCQlGH90J/uBNaMViMdRqtcHt0HVs3Ba2trbYvXs3fv/9dyxdutQkfRLcx5vQWlhY0DJamTK0QP37iTZt2oQPPvgA+/fvN1m/BHfxJrRcHGl1EhISMGvWLEyfPh03btwwad8E9/AqtHSMtGq12uShBYBPP/0Ufn5+iI+Ph0qlMnn/BHfwJrRcnR7rSCQS7Nu3D4WFheSJjkSLeBPap6fHSqUS+fn5Lb4g6969e8jPz29007tGo4GFhYVRa22Oh4cHtm/fjvT0dGzevJmRGgj24+T7af/880/s27cPFRUVqKiogEKhQHZ2NiorKyGVSlFVVdVwrXXFihVYtmxZk+2sXLkSy5cvB1C/qMLOzg61tbVwcHDA888/D7lcDkdHRzg6OmLs2LEICgoyyfYtW7YMH330Ec6cOYPg4OCGn2u1WrzzzjuIi4tDnz59DOpj8+bNWLFiRaNr0rrnPT/57GiRSITly5dj1qxZBvVH0IjioLy8PEokElEikYgSCAQUgGb/y8zMbLadzMzMFj8rEAga+snLyzPZ9mm1WioqKory9PSkSktLKYqiqJKSEmrIkCEUAGrx4sUG93Hnzp1W951uH9y5c8fg/gj6cDK0FEVRo0ePpiwsLFr8wtnZ2VFarbbZNrRaLeXg4NBiGxYWFtTo0aNNuGX1ysrKKE9PT2rEiBHU2bNnqQ4dOjRsb6dOnai6ujqD+wgJCWkxuAKBgOrTpw8NW0PQibPHtIsWLWrxEo9YLEZ0dHSLj1QVCoWIjIxs8cSTWq1GUlKSQbXqw8nJCenp6fj5558RHh4OhULRsL0FBQW4ePGiwX1MnTq1xdVfIpEIU6dONbgfgl6cDe2gQYPQo0ePZkNJURRiYmJabSc6OrrZ5Y8CgQB+fn4IDw83qFZ9KJVKrF+/HlqtFnV1dY2OPS0tLbFnzx6D+3jllVdavD2wrq4O48ePN7gfgmZMD/WG2LhxIyUUCpud2hUUFLTaRmFhYbNTRJFIRG3atMkEW9JYTk4O5efnR4nF4manrnRNkSMiIiiRSNTktg8dOpSGrSHoxunQqlQqys7Orskvtb+/f5vb8fPza7INW1tbSqlUGnELnrVnzx5KKpU2GaSn//v9998N7m/r1q1N/sMnEomobdu20bBFBN04Oz0GAKlUitmzZz9zXdXS0hJxcXFtbicuLg6WlpaNfmZhYYG5c+fS8izl9ujVqxd69+7d6lMtLC0tsXfvXoP7GzNmTJPH9EKhEKNGjTK4fcIImP5Xw1C5ublNjhS//vprm9s4duwYqy511NXVUdu3b6dsbW1bPEPeuXNnWqbIo0aNajQVF4vF1KhRo2jYEsIYOB9aiqKoESNGNPpySyQS6uHDh23+/MOHDykrK6tGX9rY2FgjVtw2hYWFVFxcXMM/Ik0F9+LFiwb3s3fv3kbtCwQCau/evTRsAWEMnJ4e6yQlJTVcDhEKhRg8eDAkEkmbPy+RSDBw4MCGM9EajYaRyzxP69ixI3744Qfs3r0b9vb2z0xj6TqLPGLEiEZvZ5BKpRg+fLjB7RLGwYvQvvTSS/Dz84NAIIBIJNLrCzd8+HAIhUIIBAJ069YNERERRqhUP+PHj0dOTg4mTJgAAA3/uDx+/Bg7d+40uH0rKyuMHTsWFhYWsLCwwLhx4wx6zy9hXLwILQAkJiYCqF8MERkZ2e7PR0VFNdwllJycDIFAQGt9hnJ1dcU333yDAwcOwNnZueHkW35+Pi5dumRw+wkJCVCr1VCr1UhISDC4PcJ4OHnDQFOUSiU6dOgAOzs7vR+U5u7ujqqqKhQVFdHy2kxjefDgAf75z3/iq6++AkVRWLJkCVavXm1QmxqNBm5ubgCA4uJiRm5PJNqGN//PyGQyzJo1y6A308XFxUEqlbI6sABgb2+PTZs2YcKECZgxYwZ2795tcGjFYnHDCEsCy268GGkLCgqQk5ODq1ev4vr16+jVq5de7fz555/o0aMHnn/+efj7+8Pd3Z3mSumnUqnw7rvvIiEhAYGBgU3+jm7/lJWVoaKiotm2bt++DYFAAG9v72Z/x9HREc7OzpzZP7zE6LlrA/zxxx/U/PnzKY/OXVpdOaTvfx6du1ALFiyg5bKKsanV6kZ/JvuHvzg30l67dg3JSUn45dgxPOfljbH9wzEksDee9+oKFzt7WvoorXqA7Ny7OJF1CfvOncbV3DuIHDYM69LS0LNnT1r6MBayf/iPM6FVqVRYunQpNny2AYHePlg7Yw4iXuhtkr6P/3UJqVu+RNbdW1jw+utYtWoVLW+dpxPZP+aDE6EtLCzEyyNH4u6tW1gzbTZejRoOocC0V6vqqDp8dfQnLNm+EV27dcPBH35gzTEd2T/mhfWhvXz5MuJiY2ErtsCht9bApyOzX4Tb9wsR9+4SVKvVOPTjYb1PetGF7B/zw+rQ5ubmIvTFF/F8Jw/sW/ouHGzYcSmmskaJsaveQnZBHjIvXICXlxcjdZD9Y55YG9rq6moM6N8fooePcfr9TyBj2bI61aOHGPxmEpQCCucyMuDg4GDS/sn+MV+sXcY4c8YMlN4vwqG3VrPuCwkA1hIr7F+6ApWKUrzGwONFyf4xX6wM7bFjx7B33z5sT1mCzi5ypstpVmcXObanLMHefftw7Ngxk/VL9o95Y930WKPRIKhXL3R3cMb+pSuYLqdNRq9chpxyBf7KyjL62wnI/iFYN9Lu3LkTN2/exIevzmO6lDb7aNZ83L59G99++63R+yL7h2DdSNu/bz94SWX4NnU506W0y8T330XeYxXOnjtn1H7I/iFYNdLev38f5y9kYtLgl5gupd0mDxmGjPPncf/+faP1QfYPAbAstCdPnoRYJEJEL9Msv6NTRK/eEItEOHnypNH6IPuHAFgW2qysLPh7eEFq2fbnO7GF1FICfw8vZGdnG60Psn8IgGWhLSoqQmdnF6bL0FsnZ2cUFRUZrX2yfwiAZaGtqamBjcSK6TL0JpNIUV1dbbT2yf4hAJaFFgBY9jy1djFF7WT/EKwLLUEQLSOhJQiOIaElCI7hdWjPXL2CVbu+fubneYpizN/wMQMVsQvZP9zE69CGPReAkgcVWJn+/9n787iY9/9//L9NM21alGRJKhXJLruEOqgwnEqpcGSyH0scHD4cb+fYjy37yRJ6IW22kAihkF0lWVpEadNeWuf5/cOvfsdRqeY5z+fM9LheLv440/S43+d55tbzOc95Ph8Pn5rHUrIyMOXv9Vhi58RiZ5KBbB/pJNOhBYDdcxYhqyAPG3x9at6Q3ktWorOOLtutSQSyfaSPzIcW+PrGTM5Mx4gVi8gbshZk+0iXZhHalKwMxH9IwYiefRBwN5ztdiQO2T7SReZDm5KVAdet63HUYwWOL12FjLwcbPI7yXZbEoNsH+kj06H99xvSRFcPwNdDQfLG/IpsH+kk06EVCqlv3pDVds9ZhL5GnVnqSnKQ7SOdZHpNQ4O27er8mW3/QQx2IpnI9pFOMr2nJQhZREJLEFJGokLL5XJRWSVku40mq6wSinUVdbJ9CEDCQquhoYG8kiK222iyvJIisS5/QbYPAUhYaA0NDfEm9SPbbTTZ648fYGRkJLbxyfYhAAkLrZmZGVKzMpGSlcF2K42WkpWBtOws9O3bV2w1yPYhAAkL7bBhw6CmqoqLDyLZbqXRLtyPgJqqKszNzcVWg2wfApCw0CoqKmKSoyMOh15mu5VGO3LtCiY5OkJRUXzTm5LtQwASFloAWLhwIWKTE6XqwvWAu+GITU7EwoULxV6LbB9C4tbyAQCBQICwy1fw6h8fqChJ9pShX8rL0G3udFiNtcXRo0cZqUm2T/MmcXtaANi8eTPyv5Rgve8Jtlv5oT9PHUdOcRE2bdrEWE2yfZo3iQxt27ZtscvTE38H+uJ0eBjb7dTpdHgY/g70xS5PT7Rt25axumT7NG8Se3mKQCBAXFwc3D3/hn6btjDv1pPtlr4RGRcDd8+/sXTpUggEAsbrk+3TfEnkZ9pqQqEQDvb2uH7tGk4tX4OJg4ex3RIA4MKDCEzZtgGjx4xB0NmzkJNj54CFbJ/mibtu3bp1bDdRFw6Hg0mTJuFjaip+99wOZQVFDDXtAQ5L60tQFIVtQWcwe892zBAIcPz4cXC5XFZ6Acj2aa4kek/7b56enli+fDnMjLtg75xFGGhiymj9h69fYaHXHjx99wbbtm2Dh4cHo/V/hGyf5kNqQgsAMTExWLxoEcJv38bk4Vb4dbwdzLuJb89CURQi42Kx/9I5+N25iZEjRmD3nj3o2VOyPj9WI9uneZCq0FYLDAzEls2b8eTpU7TXao2RPfugu54BtFvScwdJZl4u4j68R3jMc3z6nI3+/frh95UrMWnSJFrGFzeyfWSbVIa2WnR0NIKDg3H/3j28ef0a6RkZKCwS7dY1NVVVtG3TBiZdu2LI0KHg8/no1avXN89JTEzEnTt34ObmJlItcfvv9snKzkZefr5IY2q0bInWWlr1bh9CvKQ6tP/l5uaGzMxMXLlypUm/b2tri3bt2uHYsWP1Pm///v1YvHgxQkND8dNPPzWpliRycvq6FIi/vz/LnRD1kalz8VlZWWjTpk2Tf19bWxvZ2dk/fN6vv/4KV1dXuLq64uNH6b2/lZBOMhdabW3tJv++trY2srKyGvTcAwcOoHXr1pg0aRLKy8ubXJMgGkumQpuZmclYaFVVVXHu3Dm8evUKK1asaHJNgmgsmQotk3taAOjSpQsOHTqE3bt34+RJMiM/wQyZCW1JSQlKSkpE/kxbWFiI0tLSBv/O5MmTsXDhQsybNw9xcXFNrk0QDSUzoc3MzAQAkfe0ABp0MurfduzYgT59+sDe3h4FBQVNrk8QDSEzoc3NzQUAtGrVqsljVP9uTk5Oo35PXl4evr6+yMnJwezZs5tcnyAaQmZCW1hYCABQU1Nr8hiqqqoAgKImXKChq6uLM2fOIDAwEPv27WtyDwTxIzIT2uqgVQevKUQJLQBYWVlh3bp1WLp0KSIjpW/GREI6yFRoORwOlJWVmzyGqKEFgNWrV2PcuHFwdnZu1JlogmgomQltcXExWrRoIdIN11wuF0pKSiKFlsPhwNvbGwoKCnB2dkZVVVWTxyKI2shMaIuKikQ6NK6mqqoqUmgBQFNTE2fPnsX9+/fx119/idwTQfwbCe1/qKioiBxaAOjduzd27dqFDRs2ICQkROTxCKKazIS2uLgYKioqIo+jqqqK4uJiGjoC5syZg19++QVTpkxBUlISLWMShMyEtqSkBC1atBB5HBUVFdpCCwAHDx6EgYEB7O3t8eXLF9rGJZovmQltZWUlLQsWc7lcWk8eKSkpwd/fH0lJSVi6dClt4xLNl8yEtqqqipbQ8ng8VFZW0tDR/5+xsTF8fHzg5eWF48eP0zo20fzITGjp2tPyeDyxfE0zYcIE/Pbbb5g3bx6ePXtG+/hE8yFToaVjjl0ul0v7nrba5s2bMWjQIDg5OSFfxLmaiOZLZkJL5+GxuC6I4PF48PPzQ0lJCX755RfI0PRcBINkJrR0Hh6La08LfF08KyAgACEhIdi5c6fY6hCyS6ZCK+mHx9WGDh2KjRs3YuXKlbhz545YaxGyR2ZCKxQKaQmtOA+P/23ZsmWYOHEinJyckJaWJvZ6hOyQmdDKyclBKBSKPE5VVRUji0ZxOBwcO3YMmpqacHV1FfvenZAdMhNaHo+HiooKkceh67NxQ6ipqcHf3x+PHj3C6tWrGalJSD+ZCa28vDwteysmQwsAPXv2xOHDh7Ft2zacPXuWsbqE9JKZ0Erjnraaq6srZs6cCTc3N8THxzNam5A+MhVaOva0FRUVjIcWAPbu3QsTExM4OTmhpKSE8fqE9JCZ0Err4XE1RUVFBAUFIS0tjczoSNRLZkL738PjoqIifPjwod4Fsj5+/IgPHz58c9N7ZWUl5OXlxdprXfT09HDixAn4+vriyJEjrPRASD6pXOry2bNnCAoKQm5uLnJzc5GVlYXY2Fjk5eVBWVkZBQUFNd+1rl+/HmvWrKl1nA0bNuCPP/4A8PWiCnV1dXz58gUaGhro0aMHtLW1oampCU1NTTg4OKBv376MvL41a9Zgx44diIiIQL9+/Woer6qqwp9//gk+n48BAwaIVOPw4cPYsGHDN99JV88drampWfMYl8vFmjVrMGvWLJHqETSipFBKSgrF5XIpLpdLcTgcCkCd/6KiouocJyoqqt7f5XA4NXVSUlIYe31VVVWUtbU1pa+vT2VnZ1MURVGZmZmUpaUlBYBavny5yDUSEhJ+uO2qt0FCQoLI9Qj6SGVoKYqi7OzsKHl5+XrfcOrq6lRVVVWdY1RVVVEaGhr1jiEvL0/Z2dkx+Mq++vz5M6Wvr0+NGzeOioyMpNq1a1fzejt06EAJhUKRa/Tr16/e4HI4HKp///40vBqCTlL7mXbx4sX1fsXD4/FgY2NT75SqcnJyGDNmTL0nnioqKuDh4SFSr03RqlUr+Pr64urVq7CwsEBWVlbN601NTcWTJ09ErvHLL7/Ue/UXl8vFL7/8InIdgl5SG9oRI0bA1NS0zlBSFAVbW9sfjmNjY1Pn5Y8cDgcmJiawsLAQqdemKCoqwu7du1FVVQWhUPjNZ08FBQUEBASIXMPZ2bneSz+FQiGcnJxErkPQjO1dvSgOHTpEycnJ1Xlol5qa+sMx0tLS6jxE5HK51OHDhxl4Jd96/fo1ZWJiQvF4vDoPXek6RLa0tKS4XG6tr93KyoqGV0PQTapDW1JSQqmrq9f6pu7atWuDxzExMal1DDU1NaqoqEiMr+B7AQEBlLKycq1B+u+/R48eiVzP29u71j98XC6XOnbsmOgviKCd1B4eA4CysjJmz5793feqCgoK4PP5DR6Hz+dDQUHhm8fk5eUxd+5cWuZSbow+ffrAzMzsh7NaKCgoIDAwUOR6Dg4OtX6ml5OTg52dncjjE2LA9l8NUSUnJ9e6pwgLC2vwGNevX6/18DoxMVGMnddNKBRSJ06coNTU1Oo9Q66rq0vLIfLEiRO/ORTn8XjUzz//TMMrIcRB6kNLURQ1bty4b97cioqKVGlpaYN/v7S0lFJSUvrmTTt+/HgxdtwwaWlpFJ/Pr/kjUltwnzx5InKdgICAb8bncDhUYGAgDa+AEAepPjyu5uHhUfN1iJycHEaOHAlFRcUG/76ioiKGDx9ecya6srKSla95/qt9+/a4ePEi/P390bJly+8OY+k6izx+/PhvVmdQVlbG2LFjRR6XB7wQvwAAIABJREFUEA+ZCO2oUaNgYmICDocDLpfbpDfc2LFjIScnBw6HA2NjY1hZWYmh06ZxdHTE69evMXnyZACo+eNSXl6OU6dOiTy+kpISHBwcIC8vD3l5eUyaNEmkdX4J8ZKJ0ALAokWLAHy9GGLMmDGN/n1ra+uau4SWLFkCDodDa3+iatOmDU6ePIlz585BS0ur5uTbhw8f8PTpU5HHd3V1RUVFBSoqKuDq6iryeIT4SOUNA7UpKipCu3btoK6u3uSJ0nR0dFBQUID09HRals0Ul/z8fCxbtgxHjx4FRVFYtWoVNm3aJNKYlZWVaNOmDQAgMzOTldsTiYaRmf8zqqqqmDlzpkgr0/H5fCgrK0t0YAGgZcuWOHz4MCZPnowZM2bA399f5NDyeLyaPSwJrGSTiT1tamoqXr9+jZcvX+LVq1fo06dPk8Z59uwZTE1N0aNHD3Tt2hU6Ojo0d0q/kpIS/PXXX3B1dUWvXr1qfU719vn8+XPN7Xe1SUhIAAAYGRnV+RxNTU1oaWlJzfaRSayeuxbB48ePqfnz51N6uh1/eOVQU//p6Xakfv31V1q+VhG3ioqKb/6bbB/ZJXV72ri4OCzx8MC169fR3cAQDkMtYNnLDD0MOqG1ektaamQX5CM2OQm3op8i6N5dvExOxJjRo7HL0xPdunWjpYa4kO0j+6QmtCUlJVi9ejX279uPXoZG+HvGHFj1NmOk9s0XT7HC2wvRSe/w64IF2LhxIy2rztOJbJ/mQypCm5aWhokTJiDp3Ttsnj4b7tZjIcdh9tsqISXE0dArWHXiEDoZG+PCxYsS85mObJ/mReJD+/z5c/DHj4caTx7BazfDqD27b4SET2ng/7UKhRUVCL58qcknvehCtk/zI9GhTU5OxqCBA9Gjgx6CVv8FDRXJ+Comr7gIDhvXIjY1BVEPH8LAwICVPsj2aZ4kNrSFhYUwHzoU3NJy3N26B6oSdlldSVkpRq70QBGHwr3796GhocFofbJ9mi+JvYxRMGMGsj+lI3jtJol7QwJAC0UlnF29HnlZ2Zg1cybj9cn2ab4kMrTXr19HYFAQTixdBd3W2my3Uyfd1to4sXQVAoOCcP36dcbqku3TvEnc4XFlZSX69umDzhpaOLt6PdvtNIjdhjV4nZOFF9HRYl+dgGwfQuL2tKdOncKbN2+w3X0e26002I6Z85GQkIDTp0+LvRbZPoTE7WmHDh4CA2VVnF7xB9utNIrL1r+QUl6CyHv3xFqHbB9Cova0nz59woOHUZgychTbrTTaVMvRuP/gAT59+iS2GmT7EICEhTY8PBw8LhdWfZi5/I5OVn3MwONyER4eLrYaZPsQgISFNjo6Gl31DKCs0PD5nSSFsoIiuuoZIDY2Vmw1yPYhAAkLbXp6OnS1WrPdRpN10NJCenq62MYn24cAJCy0xcXFUFFUYruNJlNVVEZhYaHYxifbhwAkLLQAIGHzqTUKE72T7UNIXGgJgqgfCS1BSBkSWoKQMjId2oiXMdh45n/fPZ6SlYH5+3ey0JFkIdtHOsl0aId174nM/Fxs8PWpeSwlKwNT/l6PJXZkhXOyfaSTTIcWAHbPWYSsgjxs8PWpeUN6L1mJzjq6bLcmEcj2kT4yH1rg6xszOTMdI1YsIm/IWpDtI12aRWhTsjIQ/yEFI3r2QcDdcLbbkThk+0gXmQ9tSlYGXLeux1GPFTi+dBUy8nKwye8k221JDLJ9pI9Mh/bfb0gTXT0AXw8FyRvzK7J9pJNMh1YopL55Q1bbPWcR+hp1ZqkryUG2j3SS6TUNDdq2q/Nntv0HMdiJZCLbRzrJ9J6WIGQRCS1BSBmJCi2Xy0VllZDtNpqsskoo1lXUyfYhAAkLrYaGBvJKithuo8nySorEuvwF2T4EIGGhNTQ0xJvUj2y30WSvP36AkZGR2MYn24cAJCy0ZmZmSM3KREpWBtutNFpKVgbSsrPQt29fsdUg24cAJCy0w4YNg5qqKi4+iGS7lUa7cD8CaqqqMDc3F1sNsn0IQMJCq6ioiEmOjjgcepntVhrtyLUrmOToCEVF8U1vSrYPAUhYaAFg4cKFiE1OlKoL1wPuhiM2ORELFy4Uey2yfQiJW8sHAAQCAcIuX8Grf3ygoiTZU4Z+KS9Dt7nTYTXWFkePHmWkJtk+zZvE7WkBYPPmzcj/UoL1vifYbuWH/jx1HDnFRdi0aRNjNcn2ad4kMrRt27bFLk9P/B3oi9PhYWy3U6fT4WH4O9AXuzw90bZtW8bqku3TvEns5SkCgQBxcXFw9/wb+m3awrxbT7Zb+kZkXAzcPf/G0qVLIRAIGK9Ptk/zJZGfaasJhUI42Nvj+rVrOLV8DSYOHsZ2SwCACw8iMGXbBoweMwZBZ89CTo6dAxayfZon7rp169ax3URdOBwOJk2ahI+pqfjdczuUFRQx1LQHOCytL0FRFLYFncHsPdsxQyDA8ePHweVyWekFINunuZLoPe2/eXp6Yvny5TAz7oK9cxZhoIkpo/Ufvn6FhV578PTdG2zbtg0eHh6M1v8Rsn2aD6kJLQDExMRg8aJFCL99G5OHW+HX8XYw7ya+PQtFUYiMi8X+S+fgd+cmRo4Ygd179qBnT8n6/FiNbJ/mQapCWy0wMBBbNm/Gk6dP0V6rNUb27IPuegbQbknPHSSZebmI+/Ae4THP8elzNvr364ffV67EpEmTaBlf3Mj2kW1SGdpq0dHRCA4Oxv179/Dm9WtkZWcjLz9fpDHVVFXRtk0bmHTtiiFDh4LP56NXr140dcys/26f9IwMFBaJdmufRsuWaK2lJRPbR1pJdWjpFhwcjJ9//hnv37+Hrq5sTdj98eNH6Ovr4/z58+Dz+Wy3Q4iAnIv/F2tra2hoaODMmTNst0I7X19ftGzZEmPGjGG7FUJEJLT/oqCgAHt7e/j6+rLdCu18fX0xadIkcpeNDCCh/Q8XFxc8ffoUcXFxbLdCm/j4eDx79gwuLi5st0LQgIT2P0aOHIkOHTrAz8+P7VZoc/r0aejo6GD48OFst0LQgIT2P+Tk5DB58mScOnUKsnKO7syZM3BxcSFXJ8kIEtpauLi4ICEhAY8ePWK7FZFFRUXh7du35NBYhpDQ1qJ///4wMTHB6dOn2W5FZKdPn0bnzp3Rr18/tlshaEJCWwcXFxf4+vqisrKS7VaarKqqCv7+/pg6dSrbrRA0IqGtw9SpU5GVlYVbt26x3UqT3bhxA+np6XB2dma7FYJGJLR1MDIyQv/+/aX6O1tfX18MHDgQXbp0YbsVgkYktPVwcXFBUFAQvnz5wnYrjVZaWopz586RE1AyiIS2Hs7OziguLsaVK1fYbqXRLl26hMLCQjg6OrLdCkEzcsPAD4waNQoaGhoIDAxku5VGcXBwQEFBAa5fv852KwTNyJ72B1xcXHD58mXk5eWx3UqDFRQUICQkhBwayygS2h+oPrw8d+4cy500XGBgIIRCIezt7dluhRADcnjcAPb29igqKsK1a9fYbqVBRo8eDXV1dQQFBbHdCiEGZE/bAC4uLrhx4wZSU1PZbuWHPn36hFu3bpFDYxlGQtsAfD4fampqCAgIYLuVHzpz5gxUVFQwbtw4tlshxISEtgGUlJRgZ2cnFRda+Pr6wt7eHsrKymy3QogJCW0Dubi44OHDh3jz5g3brdQpISEBjx8/hqurK9utEGJEQttAP/30E9q1ayfR80edPHkS2trasLS0ZLsVQoxIaBuIy+XCyclJog+R/f394eLiAh5PYtdVI2hAQtsILi4uiI+Px9OnT9lu5TtPnjxBXFwcOWvcDJDQNsLgwYPRuXNnidzb+vr6wsjICAMHDmS7FULMSGgbafLkyTh9+jSqqqrYbqWGUCiEn58fXF1dWVsxj2AOCW0jTZkyBWlpabh79y7brdS4ffs2Pn78iMmTJ7PdCsEAEtpG6tq1K/r06SNRh8i+vr7o27cvunfvznYrBANIaJvAxcUFAQEBKCsrY7sVlJeX4+zZs+QEVDNCQtsELi4uyM/Px9WrV9luBVeuXEFOTg6cnJzYboVgCLnLp4lGjBiB9u3bs36xxeTJk5GRkYHw8HBW+yCYQ/a0TeTq6ooLFy6goKCAtR4KCwtx6dIlctliM0NC20ROTk4QCoW4cOECaz2cO3cOFRUVcHBwYK0Hgnnk8FgEEyZMQGVlJWsTv9na2kJBQYHVPxwE88ieVgQuLi64fv06MjIyGK+dlZWFsLAwcta4GSKhFcHEiROhpKTEykyNfn5+UFRUBJ/PZ7w2wS4SWhG0aNECEydOZOVCC19fX9jZ2UFFRYXx2gS7SGhF5OLignv37iEpKYmxmu/fv8f9+/fJoXEzRUIrImtra2hra9f6fW1paanI49c2xqlTp6ClpYXRo0eLPD4hfUhoRcTj8eDg4ICTJ08C+BqyoKAg/Pzzzxg8eLDI4w8ePBg///wzgoKCagLs6+sLJycnyMvLizw+IYUoQmTh4eEUAGrChAmUiooKxeFwKDk5OapTp04ij92pUyeKw+FQHA6HatGiBTVhwgQKAHX79m0aOiekEfmeVgQvX77E//73P3h7eyMrKwtcLveb+2w7deqExMREkWp06tQJycnJNf/N4/FQWVkJdXV1ODs7Y9q0aTA3Nyf30TYj5PC4Cfz8/KCvr48ePXpg165dyMrKAoDvbowXx9/D6pXpCwoKcPz4cVhYWMDAwAB+fn601yIkEwltE1hYWKCkpARycnIoLy9nrY/y8nLIycmhpKQEFhYWrPVBMIuEtgl0dHQQHBwMLpcr9lo/2ltzOBycPXsWOjo6Yu+FkAwktE00ePBgHD58mO02cODAAbKXbWbIBLkimD59OiIjI+Ht7V3rRG90fKatawwej4cZM2Zg9uzZIteQJlVVVXj//j0yMjJQXFyM3NxcAICCggJUVFTQqlUrdOzYEdra2ix3Kj4ktCLav38/YmNj8fjxY1RUVDBSU15eHn369MHevXsZqcemV69e4datW7hz5w5iY2Px7t27Bk3z06pVK3Tp0gX9+vXDyJEjMXLkSLRu3ZqBjsWPfOVDg/T0dPTu3RufP3/+Zo+rp6eH9+/fizS2np4ePnz4UPPfXC4XGhoaePHiBTp06CDS2JLq+fPn8PHxgZ+fH9LS0qCmpoYhQ4age/fuMDY2hrGxMdq2bQsVFRW0bNkSHA4H5eXlKCkpQW5uLlJSUpCQkIC3b9/i6dOnePHiBYRCIfr374+pU6fCxcVFqvfEJLQ0efDgAYYPH/7N3lZcob1165bMfY6tqKjA6dOnsXPnTkRHR8PQ0BCTJk3CTz/9hN69e4t00q+goAD379/H5cuXERwcjLKyMvD5fKxcuRIDBgyg8VUwg5yIokltJ6bE8Zn24MGDMhXYyspKHDx4EF26dMGsWbPQrVs3XLlyBVFRUVi+fDnMzMxEPkuvrq4Oa2tr7NmzB3Fxcdi7dy+Sk5MxcOBAWFtb4969ezS9GmaQ0NJo+vTpmD17Nq0LYFVf6cTlcjFr1izMmjWLtrHZFhkZiX79+mHJkiX46aefEBUVhb1794p176esrAwHBweEhoYiMDAQRUVFGDZsGGbMmFFzkYykI6Gl2b59+2h901Xvac3MzGTmxFNJSQnmzJkDCwsLtG7dGnfu3MGWLVvQsWNHRvsYMWIELly4AG9vb1y/fh0mJiY4deoUoz00BflMKwafPn1C7969oaysLPJnWn19fXz58gUvXrxA+/btaeqQPXFxcXByckJaWhq2bduGiRMnst0SAKC4uBgbN27EkSNHIBAIsHfvXigrK7PdVq3InlYM2rdvj4sXL0JVVVXksZSVlREUFCQTgT137hwGDhyIFi1a4ObNmxITWABQUVHBpk2b4OPjg7Nnz2Lw4MH4+PEj223ViuxpxSA5ORmvXr3C7du3YWhoKNJYiYmJGDFiBExNTWFgYEBPgyw4fPgw5s2bh2nTpmHTpk0SfS/whw8f4OrqipKSEly9ehWmpqZst/QNElqaPHjwAN7e3rgcfAlp6Z8AAPI8HlSVW4g0btGXElT8/+7s0WnXHuMn8CEQCDBo0CCRe2bKtm3b8Pvvv2PZsmVYsWIF2+00SF5eHqZOnYp3797h2rVrMDMzY7ulGiS0IoqJicHiRYtwKzwcvY06Y9LQ4bDs3Re9OhlBTcTAViv8UoLopATcevEMgffu4EXCW1iOHInde/agZ8+etNQQlyNHjmD27NnYsGGD1F1y+eXLF0ybNg3x8fGIiIiAsbEx2y0BIKFtsqKiIqxcuRJeXl7o19kE2wXzMKw7MwGKeBmDZUcP4MnbN5gzdw62bNlCy+dnul24cAEODg5YunSp1Oxh/6uoqAh2dnYoKChAZGQk2rVrx3ZLJLRN8fHjR/DHj0fq+xRsE8zFLz9ZMz5zBEVR8LkRiuXe/6CDvh6CL12Crq4uoz3U5/Xr1+jfvz8cHBywfft2ttsRyefPnzF27Fh07NgRN27cYOSWzPqQ0DbSkydPwB8/HlrKKgheuwkGbdn9y5uckQ7+X6vw+UsJgi9dQr9+/VjtB/g6ud2QIUPA4XBw6dIlKCgosN2SyF69egVra2ssW7YMf/31F6u9kK98GiExMRE21tbo2UEPkdv2sh5YADBo2w6R2/ahZwc92FhbizwnFR1+++03JCcn4+jRozIRWAAwNTXF+vXrsXHjRty+fZvVXsietoEKCgpgPmQo5MsrcPfvvVBRUmK7pW98KS/DyJUeKBBW4X7UA2hoaLDSR1RUFIYOHYoDBw7I5Gp+U6dORUpKCp4/f87aHySyp20gt+nTkZuVhUv/t1niAgsAygqKOLd6PQpzcyGYMYOVHoRCIRYtWoRBgwbB3t6elR7EbcuWLXj//j127tzJWg8ktA1w9epVnDt/Hj5LV0FHS3JvpNbRag2fpatw7vx5XL16lfH6x44dw/Pnz7F9+3aZndJVV1cXS5YswYYNG5CZmclKD+Tw+AcqKirQs0cP9Gyrg4BVf7LdToNM2rQWLz59ROzLl1BUVGSkZlVVFUxNTTFo0CDs2rWLkZpsKSsrQ79+/SAQCLBp0ybG65M97Q+cPHkSycnJ2CaYx3YrDbbdfT4+fPiA06dPM1bT398fiYmJWLhwIWM12aKoqIhZs2Zh//79NXNUMYmE9ge8Dv6DSeYjJOJMcUMZtG0HB/PhOPSPF2M1PT09MWHCBJGvtZYW7u7u4HA48Pb2Zrw2CW09UlNT8fDxI0yxlL7V6aaMHI2oRw+RlpYm9lpv3rzBw4cPMW3aNLHXkhSqqqqYOHEijh07xnhtEtp63L59G/I8Hix79WW7lUaz7N0X8jwewsPDxV7r+PHjaN++PYYOHSr2WpLEyckJL1++RHR0NKN1SWjrER0dja56+lCSwgsElBUU0VVPH7GxsWKv5e/vDycnJ9Yv72PawIEDoa+vz/g6SiS09UhPT4eulvROtdmhVWtkZGSItUb1dKVWVlZirSOJOBwOLC0tcevWLUbrktDWo6SkBC0UmPnKRBxUFJVQWFgo1ho3btyAkpKSRFzzzIZhw4bh0aNHKCgoYKwmCe0PSPM1Akz0fvfuXQwYMICx74MlzbBhw1BZWcnoNKwktIRIXr58ie7du4tt/I8fP2Lx4sU16/LWJiMjAwEBAfD09PxmAe7GPqcptLS00K5dO7x69Yq2MX+EhJYQyZs3b8Q2o4NQKMSCBQtw+vRpCIXCWp/j4+ODGTNmwNDQEIsXL651Hq2GPEcURkZGeP36Na1j1ocswEU0WUZGBvLy8mBkZCSW8Q8ePIjPnz/X+jOKojB9+nQUFRXh3LlztR6eN+Q5dDA2NkZ8fLxYxq4N2dMyKOJlDDae+d93j6dkZWD+fvbuGmmq6jPT4piCJS4uDtHR0XXe3rd//348fvwY//zzT51hbMhz6NC2bVtGbx4goWXQsO49kZmfiw2+PjWPpWRlYMrf67HEzonFzpqmqKgIwNc5g+lUXl6OdevWYfPmzbX+PDo6Gps2bcL8+fPRpk2bJj+HLqqqqmI/S/9vJLQM2z1nEbIK8rDB16cmsN5LVqKzjuTM79RQ1W9UuieV27BhA+bPn49WrVrV+vN//vkHFEVBX18fCxcuxMSJE7F27dpvvnZpyHPoQkLbDOyeswjJmekYsWKR1AYW+LpHBEDrxON37twBAIwcObLO5zx9+hStW7eGUCjEli1bMH/+fBw7dgwTJkyoOcvckOfQRUFBoUELXdOFhJYFKVkZiP+QghE9+yDgbjjb7TRZ9R62pKSElvHy8vJw4MABrFmzps7n5OfnIzExERYWFpg4cSJUVFRgbW0NgUCAly9f4uzZsw16Dp2Ki4uhpqZG65j1IaFlWEpWBly3rsdRjxU4vnQVMvJysMnvJNttNUl1aIuLi2kZb8OGDeBwOFi/fj3++OMP/PHHH7h+/ToAYN26dfD19UV+fj4oivru0Ll6xYXY2NgGPYdOhYWFjM47TULLoH8H1kRXD8DXQ2VpDa66ujqAr3s/OmhqaqK8vBxxcXE1/6rPyr569QopKSno2LEjVFVVkZ6e/s3vVi8v2qJFiwY9h06FhYU124IJ5HtaBgmF1DeBrbZ7ziKEPI5iqaum09fXB5fLRXJyMnr06CHyeKtXr/7uMU9PT2zcuBF+fn41sx8OGTIEMTEx3zwvNTW15mccDueHz6FTUlISozf/kz0tgwzatvsusNVs+0vPglrVlJSUoKuri3fv3jFad8uWLcjMzERgYGDNY9evX8fIkSMxYsSIBj+HLgkJCTAxMaF1zPqQPS0hElNTU8ZDq6enBy8vL/z555/49OkT0tPTkZOTAx8fn0Y9hw5CoRCJiYkktIT0MDMzE+tN4B4eHvDw8Pju8TFjxmDkyJFISkpCx44da/2c2pDniOrly5f48uULo0thksPjevB4PFRW1X6hujSorBKCxxPv32VLS0skJCQwMhfVfykoKMDExKTeMDbkOaK4c+cOtLS0aPlM31AktPVo2bIl8kqK2G6jyXKLi8S+PIi5uTkUFRUREREh1jqSKiIiAlZWVpCTYy5KJLT1MDIywuuPH9huo8lep6aI7Q6casrKyhgxYgQuXbok1jqSKD8/HxEREbC1tWW0LgltPfr164e07Cy8zxTvPEvikJyRjk/Z2Yx81po6dSrCwsKQk5Mj9lqS5MKFCwAAOzs7RuuS0NbD3NwcaqqquHBf+g79LjyIgLqaGszNzcVey97eHkpKSrRfHijpAgIC8PPPPzO+QiEJbT0UFBTgNHkyDl+7DGla8oiiKBy5dgWOTk6MLMeooqICZ2dnHDlyBFVVVWKvJwmeP3+OBw8eQCAQMF6bhPYHFi5ciLj3SfC7c5PtVhrM785NxL1PYnRdnd9//x3v379HcHAwYzXZtGvXLpiZmWHUqFGM1yah/YHevXtDIBBgufc/KC4tZbudH/pSXoaVJw7D3d0dvXv3ZqyukZERHB0dsWvXrjrnc5IVcXFxCAkJwdq1a1lZ0pMsddkAmZmZ6NK5M2aOssX2mfPZbqdey44cwJGwELx5+1bsMzb8V1xcHPr06YONGzdiBksLW4sbRVGws7NDeXk5oqKiWAkt2dM2QJs2bbBn717sPOcPnxuhbLdTJ58bodh5zh979u5lPLAA0K1bNyxevBibNm1CdnY24/WZEBgYiPv372Pv3r2sLZxN9rSNsGrVKuzauRPXNmzH8B7MHXo2xJ3YFxizZhmWLF1a59xKTCgqKoKpqSkGDhwILy/mltpkQnZ2NkaMGAF7e3scPHiQtT5IaBtBKBRispMTrly+jBNLV2HSsJFstwQACIwIx/SdmzF23Dj4+fszenVObUJDQzF27Fjs2LEDU6dOZbUXugiFQkyePBkpKSl4+vQpWrZsyVov3HXr1q1jrbqU4XA4cHBwQEZmJlbs3AYel4th3XuydpgkpITY5HcS8/fvwpy5c3Hk6FGJWLnO2NgYZWVl2LhxI6ytraGtLb2LmFXbtWsXAgICEBISwvrC2WRP20T79+/HEo8l6NnJEHvmLIR5t56M1o+Mi8Eir72ISUrELs9d+PXXXxmt/yOVlZUYNWoU3r17hytXrkBHR4ftlprs3LlzmDt3Lnbv3o0FCxaw3Q4JrShevXoFj8WLcT0sDPbmw/HreDuM6NkbchzxHJ4KKSFux7zA/kvncDbyDkaPGgXP3bthamoqlnqiys/Px4gRI1BaWorg4GBoamqy3VKjRUREwNnZGTNnzsS+ffvYbgcACS0tLly4gC2bN+NBVBS0NTQxokdv9NDvBO2WGuCJeLhaUVWJrPw8vHyfjNuxL5CVl4shgwfj95UrMXHiRJpegfh8/PgR5ubm0NTUxJkzZ9C6dWu2W2qw8PBwuLm5YeLEiTh58iRrH4P+i4SWRq9evUJwcDAe3H+AV3Ev8TknR+Q5dnk8Hlq0aAEzs34YPGQw+Hy+xO5Z65KQkABra2sAX1eN19OrfcodSXL27FksXLgQjo6OOHbsGK1zO4uKhFbC+fj4YNasWUhNTZWqvdR/ZWRkwNbWFqmpqTh06BAjNzI0RVVVFXbu3Int27fDw8MD27dvl5g9bDVycYWEc3R0RIsWLWif24hpbdu2RXh4OIYNGwYHBwds375d4i53zMzMhKOjI3bv3o29e/dix44dEhdYgIRW4ikrK2Py5Mk4fPiwVN1pVBt1dXUEBQXB09MTnp6e4PP5ePnyJdttgaIo+Pr6YsSIEfj06RPu3buH+fMl93JVElop4O7ujvj4eNy/f5/tVmixYMECPHz4EFwuF6NGjcKaNWtYu4H+xYsX4PP5WLJkCVxcXPDkyRNGJ2lrChJaKTBgwAD06dMHR48eZbsV2vTq1QuRkZH4559/EBQUBDMzM6xbt46xdV4fPXoEFxcXjBo1CnJycnj06BH27NnD6EoBTUVCKyUEAgH8/PzEslQjWzgcDtzd3ZGUlIQ///wTZ8+VeBQJAAAgAElEQVSeRb9+/TBz5kyEhoaioqKC1nqfP3/GkSNHMGbMGIwdOxZfvnzBlStXcO/ePfTt25fWWuJEzh5Liby8PHTo0AG7du3C7Nmz2W5HLEpLS+Hm5oYbN24gJycHmpqasLCwgIWFBczNzRs9SV15eTmeP3+Ou3fv4u7du3j48CGUlJTg4OAAgUAACwsLMb0S8SKhlSJTp07F27dvERUlfev+NARFUejatSusra3x22+/wd/fHzdv3kRERASKiorQokULdO7cGYaGhtDR0YGKigpUVVXRokUL5Ofno6ioCEVFRUhKSkJiYiJSUlJQWVmJjh07wtLSEjY2Npg4caLY5kBmCgmtFAkPD4elpSWePXuGPn36sN0O7cLCwjB69GjExMR8M/l3RUUFnjx5gpcvX+L169d48+YN0tLSakJaVPR1fmc1NTWoqanBwMAAXbt2hYmJCfr27QtjY2MWXxX9SGilCEVRMDExga2tLXbv3s12O7RzdHREeno67t69y3YrEo2ciJIiHA4HAoEAPj4++PLlC9vt0Co9PR0XLlzAnDlz2G5F4pHQSpkZM2aguLgY586dY7sVWnl7e0NFRQX29vZstyLxyOGxFLKzs0NBQQFu3LjBdiu0EAqFMDY2hp2dHXbs2MF2OxKP7GmlkLu7O27dusX4urDiEhoaiqSkJLi7u7PdilQgoZVCtra20NXVxbFjx9huhRZeXl6wtLREt27d2G5FKpDQSiEul4vp06fD29ub9quGmPbp0ydcuXKFnIBqBBJaKSUQCJCZmYmQkBC2WxHJoUOH0LJlS/z8889styI1yIkoKTZ69GgoKyvj4sWLbLfSJFVVVTAyMoKzszO2bNnCdjtSg+xppZi7uztCQkKQmprKditNcvnyZaSkpJATUI1EQivF7O3toampiePHj7PdSpN4eXlh9OjR6Ny5M9utSBUSWimmoKCAKVOm4OjRoxI3dcuPpKSkIDQ0lJyAagISWik3e/ZsJCUl4datW2y30iiHDh2CtrY2+Hw+261IHRJaKWdqaorBgwdL1awWlZWVOHbsGGbOnClRU5NKCxJaGeDu7o6goCCpWV7ywoULSE9PJyegmoiEVgY4OztDSUkJp06dYruVBvHy8oKtrS0MDAzYbkUqkdDKAFVVVUyePBlHjhxhu5UfSkxMxI0bN8gJKBGQ0MoId3d3xMbGSvxUNF5eXtDR0cHYsWPZbkVqkdDKiEGDBqF3794SfUKqvLwcx48fx6xZsyRiHV1pRUIrQ2bMmAFfX18UFhay3UqtgoKCkJOTQ05AiYiEVoZMmzYNlZWV8Pf3Z7uVWnl5eWH8+PHo0KED261INRJaGdKqVSvY2dlJ5CFyfHw87ty5Q05A0YCEVsa4u7vj/v37iI6OZruVb3h5eaFjx44YPXo0261IPRJaGWNlZQVjY2OJuomgtLQUPj4+mDt3LjkBRQMSWhnD4XDg5uYGHx8flJWVsd0OgK+rvxcWFsLNzY3tVmQCCa0Mcnd3R35+Ps6fP892KwC+Hhrb2dmhffv2bLciE8jMFTJqwoQJ+PLlC65fv85qH3FxcejevTtu3LgBKysrVnuRFWRPK6Pc3d1x48YNJCQksNrHgQMHYGRkBEtLS1b7kCUktDJq3LhxaN++PasnpEpKSnD69GnMnTsXHA6HtT5kDQmtjOLxeJg+fTqOHTuGqqqq735eWlpKa73aPmX5+vqiuLgY06dPp7VWc0dCK8NmzpyJtLS0mmlWc3NzsWfPHvTs2RN+fn601howYAA2btyIT58+1Tzm5eUFR0dHaGtr01qruSMnomSclZUVKioq0LFjRwQFBUEoFKKqqgoHDhzA3LlzaaujqKiIiooKyMnJgc/nw8bGBnPnzsWdO3ekdsV1ScVjuwFCPNLT03HixAnExMQgOzsbPB4PlZWVAL5OCEfnUpmVlZUoLy8H8HUu40uXLuH8+fNQUFDAvXv3YGJigjZt2tBWr7kjh8cyhKIoXLp0CRMmTICuri7++OOPmiloqgMLfL0Ag87QFhUVffPf1bXKy8uxZs0adOjQAQ4ODggLC6v1sy/ROCS0MoTD4eDChQsIDg5GVVVVvev8lJSU0Fb3v6H9t8rKSlRWVuLcuXPg8/l4/vw5bXWbKxJaGXPgwAFYWlrWO8shRVG0nj2uL7T/dvLkSfTt25e2us0VCa2MkZeXx/nz52FkZFRncCmKovXw+Ec33XM4HOzcuRMODg601WzOSGhlkLq6OkJCQqCmplbrXTVCoZCxw2Mul4vFixfDw8ODtnrNHQmtjDIwMMC1a9cgLy//3dVIQqFQrCeiqvF4PNjY2GD79u201SJIaGVav379ap0LmaIoFBcX01anqKjouz8M8vLy6NWrF/z9/ck9tDQjoZVx9vb22Lp163ehoju0/w6mvLw82rdvj5CQELRo0YK2OsRXJLTNwPLly7+bNaKhZ3wboqioCHJyX99KXC4XKioqCAsLIxdUiAkJbTOxd+9ejB49uuaMMt0nojgcDjgcDrhcLq5cuULWnBUjEtpmgsvlws/PD0ZGRgBA64mo4uJilJWVgcPhICAgAEOGDKFtbOJ7JLTNiLq6OkJDQ6GlpSWWiyv27t2LCRMm0DYuUTsS2mZGT08PoaGhUFJSom3MoqIi/P7775g/fz5tYxJ1I3f5NDMURUFDQwPTp0/HiRMnRJ6xUVFREW3btsXMmTNBUZRMz1AhFAqRnJyMpKQk5OTk1FwJpqamhlatWsHQ0BD6+vo1J+XEhdxP20xERETA29sbly5dQlZWFgBAWVkZCgoKIo1bXl5e8/m4TZs2GD9+PAQCAczNzUXumW0URSEyMhKhoaG4desWHj9+/M0fORUVFQDffn2mpKSEfv36wdLSEjY2Nhg6dCjtf8hIaGXcs2fPsGjRIkRERKBbt26wsrJC//79YWxsTNshcmlpKd69e4fHjx/j5s2biIuLw7Bhw7Bnzx6pvEEgOzsbBw4cwPHjx5GUlARDQ0OYm5tj4MCB6NKlCwwMDNCqVatvficnJwdJSUl4+/YtHj58iIiIiJrfdXNzw7x589C6dWta+iOhlVGFhYVYvnw5jhw5gh49esDDwwM9e/ZkpHZMTAw8PT0RGxuLmTNnYtu2bVBTU2Oktihyc3Oxfv16HDp0CMrKynBycoKzszNMTU2bNF5cXBz8/Pzg5+eH0tJSzJkzB2vWrIGmpqZIfZLQyqD379+Dz+cjLS0NHh4esLGxYfyzJkVRuHr1Kjw9PaGjo4Pg4GDo6+sz2kNj+Pj4YPny5QCARYsWYdq0abRdzVVSUgIfHx/s3bsXHA4H27Ztw7Rp05o8HgmtjHn48CH4fD40NDSwc+dOtGvXjtV+0tPTsXTpUuTl5SE4OBgDBw5ktZ//ys/Px6xZs3D27FnMmDEDq1atgrq6uthqbd68GceOHYOjoyMOHTrUpFoktDLk3bt3GDRoELp27YotW7ZIzHW/JSUlWLlyJeLj4xEVFQVjY2O2WwIAJCQkwMbGBoWFhTh48CBjE9DduXMH8+bNg4aGBkJCQmBoaNio3yehlREFBQU1VyJ5eXlBWVmZ5Y6+VVZWhnnz5qG0tBQPHjwQ+XOdqF68eAEbGxu0a9cOp0+fZnya18zMTLi4uCArKwshISHo3bt3g3+XXFwhI6ZOnYqcnBzs3LlT4gILfP0+9++//0ZBQQHrk5e/efMGo0ePhrGxMc6dO8fKvMxt2rSpmWFkzJgxePv2bYN/l+xpZcCVK1cwbtw4eHl5oV+/fmy3U68nT55gzpw5uHz5MsaOHct4/czMTAwaNAhaWlo4e/Ys6x8hiouLYW9vj9zcXERFRTXoDwgJrZQrLy9Hz549YWBggE2bNrHdToOsWrUKiYmJePnyJRQVFRmrKxQKYWNjg7dv3yI0NPS771rZkpOTgzFjxsDU1BSXL1/+4RVV5PBYyv3vf/9DcnIyFi1axHYrDbZ48WJ8+PABJ0+eZLTu9u3bcfv2bRw+fFhiAgsArVq1wqFDh3Dz5k3s2rXrh88ne1opN2DAALRp0wZ//fUX2600ytq1a5GVlYWHDx8yUi85ORndu3fHkiVLJHaSuV27dmH37t2Ii4uDnp5enc8je1op9vHjRzx58gQ2NjZst9JoNjY2ePz4MVJTUxmpt3TpUujq6kr0nUi//vor2rVrh2XLltX7PBJaKXb79m3weDz079+f7VYarX///uDxeLh9+7bYa0VHR+P8+fNYu3atyDdIiJOCggLWrl2LwMBAxMbG1vk8ElopFhMTA0NDQ4l+I9ZFQUEBhoaG9b456bJ161Z069YNY8aMEXstUdna2sLU1BRbt26t8zkktFIsPT1dqtd+bd26NdLT08VaIzc3F0FBQZgzZ45U3OvL4XAwZ84cBAYGIi8vr9bnkNBKsZKSEka/MqGbkpISrbNC1ubMmTPgcrng8/lirUMnPp8PDocDf3//Wn9OQivlpGHvURcmeg8ODsaoUaOgqqoq9lp0UVNTw08//YTg4OBaf05CS8isyspKREREYPjw4WIZ/9atW7h+/bpYxrawsMDt27e/WVe4GgktIbNevHiBwsJCDB06lNZxb9++DScnJzg5OYltvV1zc3MUFhYiJibmu5+R0BIy69WrV1BQUECnTp1oHXfw4MHYsWMHrWP+V/VSpfHx8d/9jISWqPH8+XN4e3t/93h6ejq2bNnCQkeiefv2LQwMDMDj0TvpqKKiotgnF+DxeNDX18ebN2+++xkJLVGjT58+yMnJwdGjR2seS09Px5o1a+Dq6spiZ02TnZ0ttq/EmFgJUFtbG9nZ2d89TkJLfGPZsmXIzc3F0aNHawK7du3aeq+FlVRFRUU105zSrfrMtzjPgKuoqNTMrfxvJLTEd5YtW4a0tDTMnj1bagMLfL1tsXrBMWmkqKiI8vLy7x4noSW+k56ejuTkZJiZmSEsLIztdppMVVWV1tUBmVZUVFTr98sktMQ30tPTsXr1aqxduxbr1q1DTk5OrSenpIGamlqth5fSorCwsNb5okloiRr/Dmz1HMXLli2T2uDq6ekhJSWF7Taa7P379zAwMPjucRJaooZQKPwmsNWWLVuGrl27stRV03Xt2hWZmZnIz8+nfezquSPENYdEbm4uPn/+DBMTk+9+RkJL1NDR0alzFQC6rypiQq9evQB8Xc+IbhUVFQAgts/Mz58/B4fDqXkN/0ZCS8gsHR0dmJiYIDIyktZxHz16hLVr1wIALl++DG9v71qvERbFnTt3YGpqWutFHCS0hEwbNWoU7Rf1DxgwAFu3bq2Z40ogENB+1VVYWBhGjRpV689IaKUYj8dDVVUV2200WWVlJe1v9v9ydnbGy5cv8fLlS7HWoVNMTAzi4+Ph7Oxc689JaKWYhoaG2G8iF6eioiJoaGiItYa5uTmMjIwYn65VFCdPnkTnzp0xePDgWn9OQivFDA0NkZyczHYbTZacnCz2xbg4HA4WL16MkydPIiMjQ6y16JCVlQVfX18sXry4zkskSWilWP/+/ZGVlYW0tDS2W2m0tLQ0ZGdnw8zMTOy1Zs6ciZYtW2L//v1iryWqXbt2QUNDA+7u7nU+h4RWig0dOhRqamqMTENKt/DwcKipqcHc3FzstZSVlbFu3TocPnxYoj/bvnr1CsePH8f69euhpKRU5/PICgNSbtasWbh16xbOnDkjNfNFURQFZ2dnWFlZ4dChQ4zUFAqFGDJkCIRCIS5evChxNxJUVFSAz+dDXl4ekZGR9a7nQ/a0Um7hwoVISkpCaGgo2600WGhoKJKSkrBgwQLGasrJyeHYsWN49eoVNm/ezFjdhtq4cSPi4+Ph7e1NFuCSdb169cLMmTOxd+9eqbijpbS0FPv378esWbNqvdpHnLp164Z9+/Zh3759CAoKYrR2ffz9/XHgwAEcOHAApqamP3w+Ca0M2LBhA8rKyvDPP/+w3coPHThwAF++fMH69etZqe/m5oZly5Zh4cKFuHHjBis9/FtYWBg8PDywYsUK/PLLLw36HRJaGaCtrY19+/bB19cXFy9eZLudOl28eBG+vr7Yt28fqysjbN26FVOmTMH06dNZ3V7nz5/H9OnTMW3atEYdsov3chSCMVOmTKn5vKajoyNxi3I9fvwYmzdvxv/7f/8PU6ZMYbUXDoeDo0ePomXLlpg1axYSExPr/V6UbkKhELt378aWLVuwePFibN++vVG1ydljGUJRFFxcXHD+/Hn83//9n8QsOHXt2jX8+eef+Pnnn+Hr6ytRZ7l3796NFStWwMLCArt27UL79u3FWi8tLQ0eHh6IjIzE9u3bsXDhwkaPQQ6PZQiHw8GpU6cwb948rF69GocOHYJQKGStH6FQiEOHDmH16tWYN28eTp06JVGBBb6uSn/37l28f/8eQ4cOxYEDB2qdl0lU5eXl2LdvH4YOHYoPHz4gIiKiSYEFyJ5WZnl5eWHRokXo1KkTfvvtN0auPPq3p0+fYseOHUhKSsKePXswZ84cRus3VmlpKbZs2YKtW7dCU1MTCxYsgLOzM9TV1UUat6CgAGfOnMG+ffuQm5uLlStX4vfff6/34okfIaGVYW/evIGHhwdCQkIwcuRIODo6YsCAAT/8HrCphEIhHj16hICAAISHh8PW1haenp7o0qWLWOqJQ1paGrZt24ZDhw6BoijY2Nhg3LhxMDc3R+vWrRs0RnZ2NiIiInD58mWEhoZCTk4Os2fPxrJly6CjoyNyjyS0zcDly5exefNmREZGomXLlujbty+MjY2hoaEh8oLUZWVlyMvLQ0JCAp49e4b8/HwMGzYMK1euxLhx42h6BcyLioqCo6MjOnTogMePH6OqqgqGhobo0qULDAwMoKWlVTOncnFxMbKzs/H+/Xu8ffsWCQkJ4HK5MDc3x9SpU+Ho6IiWLVvS1hsJbTPy9u1bBAcH48GDB4iNjUVubi5KS0tFGpPH40FJSQmDBw/GkCFDwOfz0blzZ5o6Zs+yZcsQFBSEhIQEFBcX486dO3j06BHi4+ORnJyMrKwsFBcXA/g6qbi2tjY6deoEExMTDBgwAMOHD691JkU6kNASIvnzzz9x8OBBfPjwQeKu522qyspKdOzYEfPnz8cff/zBdjvfIWePCZG4u7sjOzsbly9fZrsV2ly8eBGZmZmYPn06263UiuxpCZFZW1tDQUGhzpXLpc348eNRVVWFkJAQtlupFdnTEiITCAS4evWqVN6M/1+pqam4evUqBAIB263UiYSWEJmdnR00NDTg4+PDdisiO3HiBDQ0NDBhwgS2W6kTCS0hMgUFBbi6uuLw4cNim3GfCRRF4dixY5g2bRoUFRXZbqdOJLQELWbOnInExETcvXuX7Vaa7Pbt23j37h3c3NzYbqVe5EQUQZsBAwage/fuOH78ONutNMkvv/yC+Ph4PHz4kO1W6kX2tARtBAIB/P39kZeXx3YrjZafn4+goKB6Z0GUFCS0BG1cXV3B4XDg7+/PdiuN5uvrC4qiMHnyZLZb+SFyeEzQatq0aXjz5g2ioqLYbqVRBg4ciK5du0rFGXCypyVoJRAI8PDhQ0RHR7PdSoPFxsbi0aNHUnFoDJDQEjQbOXIkjI2Npepk1JEjR2BoaIjhw4ez3UqDkNAStOJwOHBzc4OPjw/KysrYbueHysvLcfr0abi7u0vcrBp1IaElaOfm5oa8vDypuBb5woULyMnJwbRp09hupcHIiShCLMaNGwehUCixF91Xs7GxAY/Hw6VLl9hupcHInpYQC4FAgGvXriElJYXtVur08eNHhIWFSc0JqGoktIRY8Pl8tG7dWqK/QvH29oampqbUTYtDQkuIhYKCAqZOnQpvb29Wp3GtC0VR8PHxwfTp00WeJ4tpJLSE2MyaNQtJSUkIDw9nu5Xv3Lx5EwkJCRJ932xdyIkoQqwGDx4MY2NjnDx5ku1WvuHq6ork5GTcu3eP7VYajexpCbFyd3dHUFAQcnNz2W6lRn5+Ps6fPy+Ve1mAhJYQMxcXF/B4PPj6+rLdSo2TJ09CTk4OTk5ObLfSJOTwmBC7GTNmIDo6Gk+ePGG7FQCAmZkZ+vTpA29vb7ZbaRKypyXETiAQ4OnTp3j+/DnbrSA6OhrPnj2T2kNjgISWYICFhQW6du0qEXu2w4cPo0uXLjA3N2e7lSYjoSUY4ebmhlOnTom8DIkoysrKcObMGam6OaA2JLQEI9zc3FBYWIjz58+z1sPZs2eRl5cnVTcH1IaciCIYM2HCBJSWluLatWus1B89ejRUVFRY/cNBB7KnJRjj7u6OsLAwJCYmMl47OTkZN2/elOoTUNVIaAnGjBs3Du3atfvmJoKioiJ4e3tj48aNtNX5888/ceDAgW9mhTx27Bi0tbVha2tLWx3WUATBoBUrVlC6urrU3bt3KYFAQCkrK1MAKGtra9pq2NraUgAoBQUFysXFhbpx4walr69P/f7777TVYBOP7T8aRPORkZEB4Ot9rBYWFlBQUEB5eTkAoKSkhLY61WOVl5cjICAAvr6+4HA4KC0txcePH6Grq0tbLTaQw2NCrIRCIcLCwjBp0iR06NABO3furPlZdWAB0PpV0L//AFRWVgL4eive/v37oaenBysrK/j4+ODLly+01WQS2dMSYvX3339j1apV4HK5qKqqqvN5dO5p6/oDUB3g27dv49atW0hNTcWqVatoq8sUsqclxGrFihWYMGHCDy9moHNP+6M9qJycHOzt7bFy5UraajKJhJYQKzk5OZw5cwa9e/eGvLx8nc+jM7T1jSUvL4/u3bvjf//7n9ReFUVCS4idsrIygoODoaWlBR6v9k9kdM6RXNdYPB4PmpqauHz5Mlq0aEFbPaaR0BKMaN++PcLCwqCoqAg5ue/fduIOrZycHBQUFHD9+nV06NCBtlpsIKElGNO9e3cEBATU+rN/n0kWVV1jnTlzBr169aKtDltIaAlG2draYseOHd89Ls7Qcjgc7N69G3w+n7YabCKhJRjn4eGB+fPng8vl1jxGURQtwa2oqPhmylYul4sFCxZgwYIFIo8tKchdPgQrqqqqwOfzERYWhoqKCgBfJ1xTV1cXadzCwsKaMXg8HiwtLXHlypU6T4BJI7KnJVjB5XJx5swZGBkZ1Xz1QscVStVf93A4HHTu3BlBQUEyFViAhJZgkbq6OkJCQtCqVSsA9HxXWz2GlpYWrl69CjU1NZHHlDQktASrDAwMcPnyZSgrK9MW2hYtWuDKlSvQ09OjoUPJQz7TEqyiKAqJiYk4fPgw1NTUoK2tLdJ4WVlZyMvLw9y5c2FoaCi1Vz3Vh4SWYEVERAS8vb1x6dIlZGVlAfh65ZSoi2GVl5fXfDZu06YNxo8fD4FAINWzL/4XCS3BqGfPnmHRokWIiIhAt27dYGVlhf79+8PY2BhKSkq01CgtLcW7d+/w+PFj3Lx5E3FxcRg2bBj27NmDvn370lKDTSS0BCMKCwuxfPlyHDlyBD169ICHhwd69uzJSO2YmBh4enoiNjYWM2fOxLZt26T6BBUJLSF279+/B5/PR1paGjw8PGBjY8P4Z02KonD16lV4enpCR0cHwcHB0NfXZ7QHupCzx4RYPXz4EAMHDkRZWRlOnDgBW1tbVk4OcTgc2Nra4sSJEygrK8PAgQPx8OFDxvugA9nTEmLz7t07DBo0CF27dsWWLVsk5na4kpISrFy5EvHx8YiKioKxsTHbLTUKCS0hFgUFBRgyZAgAwMvLC8rKyix39P+1d/cxVdWPH8DfF+6DRDxcvSKWZoDRE5eironJH/1BznQ1m4882ZK0sZHZtK1yzZElG1uthTBlrfV0x8zcCEpF/xAyzZbmRiCUw4sKjAY93KsX7+XK/Xz/8Cf7oTwph/s5H3m//vKec/zwZvPt+Zx7z7mfwfx+PwoKCuDz+XDixAlYrVbZkcaM02OaELm5ufjnn3/w0Ucf6a6wAGCxWFBSUgKPx4OXXnpJdpxbwtKS5vbv34+amhoUFRXBZrPJjjMsm82GoqIi1NTUYP/+/bLjjBmnx6Spvr4+2O123H///dixY4fsOGPy9ttv49y5c2hqaoLFYpEdZ1Q805KmvvrqK7S1tWHjxo2yo4zZ66+/josXL+Lrr7+WHWVMWFrS1K5du5CZmYn4+HjZUcYsPj4emZmZ2L17t+woY8LSkmba29tx6tQpLF68WHaUW7Z48WKcPHkSHR0dsqOMiqUlzdTX18NoNMLhcMiOcsscDgeMRiPq6+tlRxkVS0ua+f3335GYmDjuJ3VkMJvNSExMRGNjo+woo2JpSTNdXV3jfh5WJpvNhq6uLtkxRsXSkmZ6e3uV+MhkOFOmTMHly5dlxxgVS0uaUvmbIlTJztISKYalJSUEg0F4PB7ZMXSBpSUl/PXXXygtLZUdQxdYWiLFsLREimFpiRRzZy1yQneM6upqtLS0DLz2er1obm5GSUnJoOPy8/Mxbdq0UMeTiqUlXUpPT0dKSsrA656eHvh8PqxYsWLQceNdZU9FLC3pUlxcHOLi4gZeR0REIDo6GomJiRJT6QOvaYkUw9ISKYalJSVYLBYkJSXJjqELLC0pYerUqVizZo3sGLrA0hIphqUlzRiNRvT398uOcduuXr0Ko1H/H6iwtKSZ2NhYJR4iH87ly5cRGxsrO8aoWFrSTGJiItra2mTHuG1tbW1KLMbF0pJmHA4Huru70dnZKTvKLevs7ERPTw+eeOIJ2VFGxdKSZp5++mlERUUp8TWkN6qrq0NUVBQWLlwoO8qoWFrSjNlsxurVq1FVVQWVlogSQuC7777DmjVrYDKZZMcZFUtLmnrttdfgcrlQW1srO8qY1dbWwuVyobCwUHaUMWFpSVOpqal45ZVXUFpait7eXtlxRuXz+VBWVob169cjNTVVdpwxYWlJc++//z78fj927dolO8qoysvLceXKFWzfvl12lDFjaUlz06dPx86dO1FZWYnq6rMxtGUAAAfJSURBVGrZcYZVXV2NyspK7Ny5U6mVEfR/+wcpKScnB83NzSguLsY999yju0W5Tp48ieLiYrzzzjvIycmRHeeWcCV4mjBCCGRlZaGqqgrbtm3DokWLZEcCABw6dAhFRUVYtmwZKisrlVlZ4DpOj2nCGAwGOJ1OFBQUYOvWraioqEAwGJSWJxgMoqKiAlu3bkVBQQGcTqdyhQV4pqUQ2b17NzZu3IiEhARs3rw55Hce/fbbb/jwww/hcrnwySef4NVXXw3pz9cSS0sh8+eff2LTpk04cOAAnnnmGaxcuRLz5s1DWNjETPiCwSB+/fVX7N27F3V1dXjuuefw8ccfIzk5eUJ+XqiwtBRyP/zwA4qLi3Hs2DHExMQgLS0Nc+fORWxs7LgXpPb7/fjvv//Q2tqK06dPw+12IyMjA2+99RaWLl2q0W8gF0tL0pw9exY1NTU4ceIEGhsb8e+//8Ln8w17/PV/qiNdh06ZMgVWqxWPPvooFixYgOeffx4PPPCA5tllYmlJGatWrQIAfPPNN5KTyMV3j4kUw9ISKYalJVIMS0ukGJaWSDEsLZFiWFoixbC0RIphaYkUw9ISKYalJVIMS0ukGJaWSDEsLZFiWFoixbC0RIphaYkUw9ISKYalJVIMS0ukGJaWSDEsLZFiWFoixbC0RIphaYkUw9ISKYalJVIMS0ukGJaWSDEsLZFijLIDEA2loaEBf/zxx6Bt7e3tAIC9e/cO2v7ggw8iNTU1ZNlkY2lJl1wu18B6tDf6+eefB72uqqqaVKXlotKkS319fbDZbLh06dKIx0VFRaG7uxsWiyVEyeTjNS3pktlsxqpVq2AymYY9xmQyYfXq1ZOqsABLSzqWnZ2NQCAw7P5AIIDs7OwQJtIHTo9Jt4LBIOLj49Hd3T3kfpvNhq6uLoSHh4c4mVw805JuhYWFITc3d8gpsslkwtq1ayddYQGWlnQuKytryClyIBBAVlaWhETycXpMupeQkIC2trZB22bPno3z58/DYDDICSURz7Ske3l5eYOmyCaTCS+//PKkLCzAMy0poKWlBQ8//PCgbU1NTXjkkUckJZKLZ1rSvYceeggpKSkwGAwwGAyw2+2TtrAAS0uKyMvLQ3h4OIxGI3Jzc2XHkYrTY1LCxYsXMWfOHADX7ku+/ufJiA8MkBJmz56N+fPnA8CkLizA0pJC1q5dO2nfMf7/OD0mZfT09AC4dvviZMbSEimG7x4TKYalJVIMS0ukGL57TBOmubkZP/74IxobGzF16lQ4HA5kZmYiIiJCdjSl8UxLmvN6vXjjjTeQm5uLpKQkbNu2DS+++CKOHDmCJ598EqdPn76tcf1+v8ZJQzO25gSRxpYsWSLmzp0rent7b9r33nvvCbPZLH755ZdbHnfz5s2iv79fi4ghHVtrLC1pqqysTAAQn3/++ZD7PR6PsFqtwm63i76+vjGP29DQICIjIyekWBM59kTg57Skqbi4OPz999+4cuUKzGbzkMfk5+fjs88+g9PpRHh4OILBIEwmE1asWAEA+PbbbxEIBBAREYFly5bh2LFjyM7OxoULF+B0OmEymbBy5Uq0traipqYGmzZtwk8//YQDBw4gOTkZeXl5CAsLw549e257bF2T/b8G3Tk6OjoEAHHvvfeOeFxRUZEAIN58803h8XjEwoULRXR09MD+zs5OYbfbRXx8vBBCiKNHj4qcnBwBQHz//feitrZWlJaWirvvvlvMnDlTOJ1OYbfbRUREhAAgli9fLoQQtz223vGNKNJMQ0MDgGs394/k+v4zZ84gKioKaWlpg/bPnDlz4OEAAMjIyEBycjIAYMmSJVi0aBEKCwuxdOlSeDweCCHQ0NCA1tZWLFiwAPv27cOhQ4due2y9Y2lJMzExMQAAj8cz4nHi/67Ipk2bBuDaty7eaKhtN4qMjER0dDRycnIAXCtkcXExAODw4cPjGlvP1E5PunL92yTOnz8/4nHXF9JKSUkZ98+88amfefPmAbj2/O2diqUlzcTExCAtLQ1erxetra3DHtfS0oKwsDA8++yzmmcwm82wWCy47777NB9bL1ha0lR5eTkMBgNKSkqG3N/e3o59+/ahsLAQjz/+OAAgOjr6ppsbhBDo7++/6e/fuM3n8w16ffz4cfj9fjz11FPjHluvWFrSVHp6OrZv344vv/wSdXV1g/Z5PB6sX78e6enp+OCDDwa2z5kzB36/H4cPH4YQAnv27MHx48fhdrvhdrvR39+P6dOnAwBOnTqFo0ePDpTV7XbjwoULA2MdPHgQDocDy5cvH/fYuiXzrWu6cx05ckQ89thjYt26daK0tFRs2bJFzJ8/X+zYseOmmxi8Xq9ISUkRAMSMGTPEF198ITZs2CCsVqvYsmWL6OnpEefOnRMzZswQVqtVfPrpp0IIIdatWyciIyPFCy+8IMrKysSGDRtERkaGcLlc4x5bz3hzBU0ot9uNpqYmzJo1a8TrTCEEGhsbkZSUhLvuugtnz57FrFmzBj1cEAgEcPXq1YFt+fn5OHjwIFwuF86cOYOYmBgkJCRoMraesbSkrOul7ejokB0lpHhNS8rq7e2F1+uVHSPkWFpSTiAQQHl5Oerr63Hp0iW8++67A5/9TgacHhMphmdaIsWwtESKYWmJFMPSEinGCKBCdggiGrv/Aej7x7jSSYjOAAAAAElFTkSuQmCC" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "naive_arithmetic_circuit = circuit.arithmetize(\"naive\")\n", + "naive_arithmetic_circuit.display_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "7a721fc6-ce6c-441e-8ce2-67fee11af6cc", + "metadata": {}, + "source": [ + "#### Our depth-aware arithmetization method finds a second circuit that is potentially faster!" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "18d08a7b-a97d-4a74-856a-79dcf7197f07", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Depth: 9 , Cost: 8.0\n" + ] + }, + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Depth: 11 , Cost: 7.5\n" + ] + }, + { + "data": { + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "depth_aware_circuits = circuit.arithmetize_depth_aware(0.5)\n", + "\n", + "for depth, cost, depth_aware_circuit in depth_aware_circuits:\n", + " print(\"Depth: \", depth, \", \", \"Cost: \", cost)\n", + " depth_aware_circuit.display_graph()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/oraqle/demo/playground.ipynb b/oraqle/demo/playground.ipynb new file mode 100644 index 0000000..73fed45 --- /dev/null +++ b/oraqle/demo/playground.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b5e62be9-cada-42d2-a2bb-7f3ee38aec51", + "metadata": {}, + "source": [ + "# Playground" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df1eaaad-6ad2-4601-8d12-3b02d9254bfa", + "metadata": {}, + "outputs": [], + "source": [ + "from galois import GF\n", + "\n", + "from circuit_compiler.compiler.boolean.bool_and import And\n", + "from circuit_compiler.compiler.circuit import Circuit\n", + "from circuit_compiler.compiler.nodes.leafs import Input\n", + "\n", + "gf = GF(5)\n", + "\n", + "xs = [Input(f\"x{i}\", gf) for i in range(11)]\n", + "\n", + "output = And(set(xs), gf)\n", + "\n", + "circuit = Circuit(outputs=[output], gf=gf)\n", + "circuit.display_graph()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce27d985-b20c-4f7e-a303-929598c61c17", + "metadata": {}, + "outputs": [], + "source": [ + "naive_arithmetic_circuit = circuit.arithmetize(\"naive\")\n", + "naive_arithmetic_circuit.display_graph()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5c0531f-f50b-4852-b81c-833a813eb235", + "metadata": {}, + "outputs": [], + "source": [ + "circuit._clear_cache()\n", + "better_arithmetic_circuit = circuit.arithmetize(\"best-effort\")\n", + "better_arithmetic_circuit.display_graph()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/oraqle/demo/small_comparison_bgv.ipynb b/oraqle/demo/small_comparison_bgv.ipynb new file mode 100644 index 0000000..516a58e --- /dev/null +++ b/oraqle/demo/small_comparison_bgv.ipynb @@ -0,0 +1,162 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0f2abd68-5065-49c2-aefa-65ca3c8be8f8", + "metadata": {}, + "source": [ + "# Compiling homomorphic encryption circuits made easy" + ] + }, + { + "cell_type": "markdown", + "id": "1f425b04-35ab-4a1c-8ed4-20ecdc7d2901", + "metadata": {}, + "source": [ + "#### The only boilerplate consists of defining the plaintext space and the inputs of the program." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18d03a72-d22a-4f54-9a68-ab31507d1e34", + "metadata": {}, + "outputs": [], + "source": [ + "from galois import GF\n", + "\n", + "from circuit_compiler.compiler.nodes.leafs import Input\n", + "\n", + "gf = GF(11)\n", + "\n", + "a = Input(\"a\", gf)\n", + "b = Input(\"b\", gf)" + ] + }, + { + "cell_type": "markdown", + "id": "7a7890f4-c770-4699-acba-ec2e6796a5bb", + "metadata": {}, + "source": [ + "#### Programmers can use the primitives that they are used to." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dd02769-50cc-4eb1-a9e0-896b944d9b28", + "metadata": {}, + "outputs": [], + "source": [ + "output = a < b" + ] + }, + { + "cell_type": "markdown", + "id": "8a26b9ca-2441-48e1-8aad-4b626755485e", + "metadata": {}, + "source": [ + "#### A circuit can have an arbitrary number of outputs; here we only have one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d00fa605-4510-4393-bdb0-4dd54a21f5f8", + "metadata": {}, + "outputs": [], + "source": [ + "from circuit_compiler.compiler.circuit import Circuit\n", + "\n", + "circuit = Circuit(outputs=[output], gf=gf)\n", + "circuit.display_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "fc7c6e33-a7ad-4e2f-a742-40653160a0ca", + "metadata": {}, + "source": [ + "#### Turning high-level circuits into arithmetic circuits is a fully automatic process that improves on the state of the art in multiple ways." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a441c9f5-de63-4253-bbb6-4b63511acc67", + "metadata": {}, + "outputs": [], + "source": [ + "arithmetic_circuit = circuit.arithmetize()\n", + "arithmetic_circuit.display_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "33a64549-4081-4fb8-9631-1f007b368dfa", + "metadata": {}, + "source": [ + "#### The compiler implements a form of semantic subexpression elimination that significantly optimizes large circuits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cd8bfaf-8113-444a-812c-b2a4fe124cec", + "metadata": {}, + "outputs": [], + "source": [ + "arithmetic_circuit.eliminate_subexpressions()\n", + "arithmetic_circuit.display_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "a89d7c56-ef33-4ac6-b06a-0f88d45aff91", + "metadata": {}, + "source": [ + "#### This much smaller circuit is still correct!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d50141a-b84a-4ac4-93e7-a7a1cf484688", + "metadata": {}, + "outputs": [], + "source": [ + "import tabulate\n", + "\n", + "for val_a in range(11):\n", + " for val_b in range(11):\n", + " assert arithmetic_circuit.evaluate({\"a\": gf(val_a), \"b\": gf(val_b)}) == gf(val_a < val_b)\n", + "\n", + "data = [[arithmetic_circuit.evaluate({\"a\": gf(val_a), \"b\": gf(val_b)})[0] for val_a in range(11)] for val_b in range(11)]\n", + "\n", + "table = tabulate.tabulate(data, tablefmt='html')\n", + "table" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/oraqle/examples/depth_aware_comparison.py b/oraqle/examples/depth_aware_comparison.py new file mode 100644 index 0000000..7b7ee72 --- /dev/null +++ b/oraqle/examples/depth_aware_comparison.py @@ -0,0 +1,33 @@ +"""Depth-aware arithmetization of a comparison modulo 101.""" + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input + +gf = GF(101) +cost_of_squaring = 1.0 + +a = Input("a", gf) +b = Input("b", gf) + +output = a < b + +circuit = Circuit(outputs=[output]) +circuit.to_graph("high_level_circuit.dot") + +arithmetic_circuits = circuit.arithmetize_depth_aware(cost_of_squaring) + +for depth, cost, arithmetic_circuit in arithmetic_circuits: + assert arithmetic_circuit.multiplicative_depth() == depth + assert arithmetic_circuit.multiplicative_cost(cost_of_squaring) == cost + + print("pre CSE", depth, cost) + + arithmetic_circuit.eliminate_subexpressions() + + print( + "post CSE", + arithmetic_circuit.multiplicative_depth(), + arithmetic_circuit.multiplicative_cost(cost_of_squaring), + ) diff --git a/oraqle/examples/depth_aware_equality.py b/oraqle/examples/depth_aware_equality.py new file mode 100644 index 0000000..8d42b23 --- /dev/null +++ b/oraqle/examples/depth_aware_equality.py @@ -0,0 +1,23 @@ +"""Depth-aware arithmetization for an equality operation modulo 31.""" + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.comparison.equality import Equals +from oraqle.compiler.nodes.leafs import Input + +gf = GF(31) + +a = Input("a", gf) +b = Input("b", gf) + +output = Equals(a, b, gf) + +circuit = Circuit(outputs=[output]) + +arithmetic_circuits = circuit.arithmetize_depth_aware(cost_of_squaring=1.0) + +if __name__ == "__main__": + circuit.to_pdf("high_level_circuit.pdf") + for depth, size, arithmetic_circuit in arithmetic_circuits: + arithmetic_circuit.to_pdf(f"arithmetic_circuit_d{depth}_s{size}.pdf") diff --git a/oraqle/examples/long_and.py b/oraqle/examples/long_and.py new file mode 100644 index 0000000..0123972 --- /dev/null +++ b/oraqle/examples/long_and.py @@ -0,0 +1,20 @@ +"""Arithmetization of an AND operation between 15 inputs.""" + +from galois import GF + +from oraqle.compiler.boolean.bool_and import And +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.abstract import UnoverloadedWrapper +from oraqle.compiler.nodes.leafs import Input + +gf = GF(5) + +xs = [Input(f"x{i}", gf) for i in range(15)] + +output = And(set(UnoverloadedWrapper(x) for x in xs), gf) + +circuit = Circuit(outputs=[output]) +circuit.to_graph("high_level_circuit.dot") + +arithmetic_circuit = circuit.arithmetize() +arithmetic_circuit.to_graph("arithmetic_circuit.dot") diff --git a/oraqle/examples/small_comparison.py b/oraqle/examples/small_comparison.py new file mode 100644 index 0000000..f28c9d8 --- /dev/null +++ b/oraqle/examples/small_comparison.py @@ -0,0 +1,19 @@ +"""Arithmetizes a comparison modulo 11 with a constant.""" + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Constant, Input + +gf = GF(11) + +a = Input("a", gf) +b = Constant(gf(3)) # Input("b") + +output = a < b + +circuit = Circuit(outputs=[output]) +circuit.to_graph("high_level_circuit.dot") + +arithmetic_circuit = circuit.arithmetize() +arithmetic_circuit.to_graph("arithmetic_circuit.dot") diff --git a/oraqle/examples/small_polynomial.py b/oraqle/examples/small_polynomial.py new file mode 100644 index 0000000..9a5cc41 --- /dev/null +++ b/oraqle/examples/small_polynomial.py @@ -0,0 +1,19 @@ +"""Creates graphs for the arithmetization of a small polynomial evaluation.""" + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input +from oraqle.compiler.polynomials.univariate import UnivariatePoly + +gf = GF(11) + +x = Input("x", gf) + +output = UnivariatePoly(x, [gf(1), gf(2), gf(3), gf(4), gf(5), gf(6), gf(1)], gf) + +circuit = Circuit(outputs=[output]) +circuit.to_graph("high_level_circuit.dot") + +arithmetic_circuit = circuit.arithmetize() +arithmetic_circuit.to_graph("arithmetic_circuit.dot") diff --git a/oraqle/examples/visualize_circuits.py b/oraqle/examples/visualize_circuits.py new file mode 100644 index 0000000..c96e9d5 --- /dev/null +++ b/oraqle/examples/visualize_circuits.py @@ -0,0 +1,80 @@ +"""Visualization of three circuits computing an OR operation on 7 inputs.""" + +from galois import GF + +from oraqle.compiler.arithmetic.exponentiation import Power +from oraqle.compiler.boolean.bool_neg import Neg +from oraqle.compiler.circuit import ArithmeticCircuit, Circuit +from oraqle.compiler.nodes.binary_arithmetic import Multiplication +from oraqle.compiler.nodes.leafs import Input + +gf = GF(5) + +x1 = Input("x1", gf) +x2 = Input("x2", gf) +x3 = Input("x3", gf) +x4 = Input("x4", gf) +x5 = Input("x5", gf) +x6 = Input("x6", gf) +x7 = Input("x7", gf) + +sum1 = x1 + x2 + x3 + x4 +exp1 = Power(sum1, 4, gf) + +sum2 = x5 + x6 + x7 + exp1 +exp2 = Power(sum2, 4, gf) + +circuit = Circuit([exp2]) +arithmetic_circuit = circuit.arithmetize() +arithmetic_circuit.to_graph("arithmetic_circuit1.dot") + + +inv1 = Neg(x1, gf) +inv2 = Neg(x2, gf) +inv3 = Neg(x3, gf) +inv4 = Neg(x4, gf) +inv5 = Neg(x5, gf) +inv6 = Neg(x6, gf) + +mul1 = inv1 * inv2 +invmul1 = Neg(mul1, gf) + +mul2 = inv3 * inv4 +invmul2 = Neg(mul2, gf) + +mul3 = inv5 * inv6 +invmul3 = Neg(mul3, gf) + +add1 = mul1 + mul2 +add2 = mul3 + add1 + +add3 = add2 + x7 + +exp = Power(add3, 4, gf) + +circuit = Circuit([exp]) +arithmetic_circuit = circuit.arithmetize() +arithmetic_circuit.to_graph("arithmetic_circuit2.dot") + + +inv1 = Neg(x1, gf).arithmetize("best-effort").to_arithmetic() +inv2 = Neg(x2, gf).arithmetize("best-effort").to_arithmetic() +inv3 = Neg(x3, gf).arithmetize("best-effort").to_arithmetic() +inv4 = Neg(x4, gf).arithmetize("best-effort").to_arithmetic() +inv5 = Neg(x5, gf).arithmetize("best-effort").to_arithmetic() +inv6 = Neg(x6, gf).arithmetize("best-effort").to_arithmetic() +inv7 = Neg(x7, gf).arithmetize("best-effort").to_arithmetic() + +mul1 = Multiplication(inv1, inv2, gf) +mul2 = Multiplication(inv3, inv4, gf) +mul3 = Multiplication(inv5, inv6, gf) + +mul4 = Multiplication(mul1, mul2, gf) +mul5 = Multiplication(mul3, inv7, gf) + +mul6 = Multiplication(mul4, mul5, gf) + +inv = Neg(mul6, gf).arithmetize("best-effort").to_arithmetic() + +arithmetic_circuit = ArithmeticCircuit([inv]) +arithmetic_circuit.to_graph("arithmetic_circuit3.dot") diff --git a/oraqle/experiments/depth_aware_arithmetization/execution/cardio_circuits.py b/oraqle/experiments/depth_aware_arithmetization/execution/cardio_circuits.py new file mode 100644 index 0000000..d53bc56 --- /dev/null +++ b/oraqle/experiments/depth_aware_arithmetization/execution/cardio_circuits.py @@ -0,0 +1,35 @@ +import time + +from galois import GF + +from oraqle.circuits.cardio import ( + construct_cardio_elevated_risk_circuit, + construct_cardio_risk_circuit, +) +from oraqle.compiler.circuit import Circuit + +if __name__ == "__main__": + gf = GF(257) + + for cost_of_squaring in [0.5, 0.75, 1.0]: + print(f"--- Cardio risk assessment ({cost_of_squaring}) ---") + circuit = Circuit([construct_cardio_risk_circuit(gf)]) + + start = time.monotonic() + front = circuit.arithmetize_depth_aware(cost_of_squaring=cost_of_squaring) + print("Run time:", time.monotonic() - start, "s") + + for depth, cost, arithmetic_circuit in front: + print(depth, cost) + arithmetic_circuit.to_graph(f"cardio_arith_d{depth}_c{cost}.dot") + + print(f"--- Cardio elevated risk assessment ({cost_of_squaring}) ---") + circuit = Circuit([construct_cardio_elevated_risk_circuit(gf)]) + + start = time.monotonic() + front = circuit.arithmetize_depth_aware(cost_of_squaring=cost_of_squaring) + print("Run time:", time.monotonic() - start, "s") + + for depth, cost, arithmetic_circuit in front: + print(depth, cost) + arithmetic_circuit.to_graph(f"cardio_elevated_arith_d{depth}_c{cost}.dot") diff --git a/oraqle/experiments/depth_aware_arithmetization/execution/comparisons.py b/oraqle/experiments/depth_aware_arithmetization/execution/comparisons.py new file mode 100644 index 0000000..295ec2e --- /dev/null +++ b/oraqle/experiments/depth_aware_arithmetization/execution/comparisons.py @@ -0,0 +1,53 @@ +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.comparison.comparison import ( + IliashenkoZuccaSemiLessThan, + SemiStrictComparison, + T2SemiLessThan, +) +from oraqle.compiler.nodes.leafs import Input + +if __name__ == "__main__": + for p in [29, 43, 61, 101, 131]: + gf = GF(p) + + x = Input("x", gf) + y = Input("y", gf) + + print(f"-------- p = {p}: ---------") + our_circuit = Circuit([SemiStrictComparison(x, y, less_than=True, gf=gf)]) + our_front = our_circuit.arithmetize_depth_aware() + print("Our circuits:", our_front) + + our_front[0][2].to_graph(f"comp_{p}_ours.dot") + + t2_circuit = Circuit([T2SemiLessThan(x, y, gf)]) + t2_arithmetization = t2_circuit.arithmetize() + print( + "T2 circuit:", + t2_arithmetization.multiplicative_depth(), + t2_arithmetization.multiplicative_size(), + ) + t2_arithmetization.eliminate_subexpressions() + print( + "T2 circuit CSE:", + t2_arithmetization.multiplicative_depth(), + t2_arithmetization.multiplicative_size(), + ) + + iz21_circuit = Circuit([IliashenkoZuccaSemiLessThan(x, y, gf)]) + iz21_arithmetization = iz21_circuit.arithmetize() + iz21_arithmetization.to_graph(f"comp_{p}_iz21.dot") + print( + "IZ21 circuits:", + iz21_arithmetization.multiplicative_depth(), + iz21_arithmetization.multiplicative_size(), + ) + iz21_arithmetization.eliminate_subexpressions() + iz21_arithmetization.to_graph(f"comp_{p}_iz21_cse.dot") + print( + "IZ21 circuit CSE:", + iz21_arithmetization.multiplicative_depth(), + iz21_arithmetization.multiplicative_size(), + ) diff --git a/oraqle/experiments/depth_aware_arithmetization/execution/equality_first_prime_mods_exec.py b/oraqle/experiments/depth_aware_arithmetization/execution/equality_first_prime_mods_exec.py new file mode 100644 index 0000000..2366ac9 --- /dev/null +++ b/oraqle/experiments/depth_aware_arithmetization/execution/equality_first_prime_mods_exec.py @@ -0,0 +1,191 @@ +import math +import multiprocessing +import pickle +import time +from functools import partial +from typing import List, Tuple + +from matplotlib import pyplot as plt +from sympy import sieve + +from oraqle.add_chains.addition_chains_front import chain_depth, gen_pareto_front +from oraqle.add_chains.addition_chains_mod import chain_cost, hw + + +def experiment( + t: int, squaring_cost: float +) -> Tuple[List[Tuple[int, float, List[Tuple[int, int]]]], float]: + start = time.monotonic() + chains = gen_pareto_front( + t - 1, + modulus=t - 1, + squaring_cost=squaring_cost, + solver="glucose42", + encoding=1, + thurber=True, + ) + duration = time.monotonic() - start + + return [ + (chain_depth(chain, modulus=t - 1), chain_cost(chain, squaring_cost), chain) + for _, chain in chains + ], duration + + +def experiment2( + t: int, squaring_cost: float +) -> Tuple[List[Tuple[int, float, List[Tuple[int, int]]]], float]: + start = time.monotonic() + chains = gen_pareto_front( + t - 1, + modulus=None, + squaring_cost=squaring_cost, + solver="glucose42", + encoding=1, + thurber=True, + ) + duration = time.monotonic() - start + + return [ + (chain_depth(chain), chain_cost(chain, squaring_cost), chain) for _, chain in chains + ], duration + + +def plot_specific_outputs(specific_outputs, specific_outputs_nomod, primes, squaring_cost: float): + plt.figure(figsize=(9, 2.8)) + plt.grid(axis="y", zorder=-1000, alpha=0.5) + + for x, p in enumerate(primes): + label = "Square & multiply" if p == 2 else None + t = p - 1 + plt.scatter( + x, + math.ceil(math.log2(t)) * squaring_cost + hw(t) - 1, + color="black", + label=label, + zorder=100, + marker="_", + ) + + for x, outputs in enumerate(specific_outputs): + chains, _ = outputs + for depth, cost, _ in chains: + plt.scatter( + x, + cost, + color="black", + zorder=100, + s=50, + label="Optimal circuit" if x == 0 else None, + ) + if len(chains) > 1: + plt.text( + x, + cost - 0.05, + str(depth), + fontsize=6, + ha="center", + va="center", + color="white", + zorder=200, + fontweight="bold", + ) + + plt.xticks(range(len(primes)), primes, rotation=50) + plt.yticks(range(2 * math.ceil(math.log2(primes[-1])))) + + plt.xlabel("Modulus") + plt.ylabel("Multiplicative cost") + + ax1 = plt.gca() + ax2 = ax1.twinx() + for x, outputs in enumerate(specific_outputs): + _, duration = outputs + ax2.bar(x, duration, color="tab:cyan", zorder=0, alpha=0.3, label="Considering modulus" if x == 0 else None) # type: ignore + for x, outputs in enumerate(specific_outputs_nomod): + _, duration = outputs + ax2.bar(x, duration, color="tab:cyan", zorder=0, alpha=1.0, label="Ignoring modulus" if x == 0 else None) # type: ignore + ax2.set_ylabel("Generation time [s]", color="tab:cyan", alpha=1.0) + + ax1.step( + range(len(primes)), + [squaring_cost * math.ceil(math.log2(p - 1)) for p in primes], + zorder=10, + color="black", + where="mid", + label="Lower bound", + linestyle=":", + ) + + # Combine legends from both axes + lines, labels = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() # type: ignore + ax1.legend(lines + lines2, labels + labels2, loc="upper left", fontsize="small") + + plt.savefig(f"equality_first_prime_mods_{squaring_cost}.pdf", bbox_inches="tight") + plt.show() + + +if __name__ == "__main__": + run_experiments = False + + if run_experiments: + multiprocessing.set_start_method("fork") + threads = 4 + pool = multiprocessing.Pool(threads) + + primes = list(sieve.primerange(300))[:30] # [:50] + + for sqr_cost in [0.5, 0.75, 1.0]: + print(f"Computing for {sqr_cost}") + experiment_sqr_cost = partial(experiment, squaring_cost=sqr_cost) + outs = list(pool.map(experiment_sqr_cost, primes)) + + with open(f"equality_experiment_{sqr_cost}_mod.pkl", mode="wb") as file: + pickle.dump((primes, outs), file) + + for sqr_cost in [0.5, 0.75, 1.0]: + print(f"Computing for {sqr_cost}") + experiment_sqr_cost = partial(experiment2, squaring_cost=sqr_cost) + outs = list(pool.map(experiment_sqr_cost, primes)) + + with open(f"equality_experiment_{sqr_cost}_nomod.pkl", mode="wb") as file: + pickle.dump((primes, outs), file) + + # Visualize + with open("equality_experiment_0.5_mod.pkl", "rb") as file: + primes_05_mod, outputs_05_mod = pickle.load(file) + with open("equality_experiment_0.75_mod.pkl", "rb") as file: + primes_075_mod, outputs_075_mod = pickle.load(file) + with open("equality_experiment_1.0_mod.pkl", "rb") as file: + primes_10_mod, outputs_10_mod = pickle.load(file) + + with open("equality_experiment_0.5_nomod.pkl", "rb") as file: + primes_05_nomod, outputs_05_nomod = pickle.load(file) + with open("equality_experiment_0.75_nomod.pkl", "rb") as file: + primes_075_nomod, outputs_075_nomod = pickle.load(file) + with open("equality_experiment_1.0_nomod.pkl", "rb") as file: + primes_10_nomod, outputs_10_nomod = pickle.load(file) + + # All the primes should match + primes = primes_10_mod + assert primes == primes_05_mod + assert primes == primes_075_mod + assert primes == primes_05_nomod + assert primes == primes_075_nomod + assert primes == primes_10_nomod + + # All the chains should match (not in theory, but for this visualization they should) + assert all( + all(x == y for x, y in zip(a[0], b[0])) for a, b in zip(outputs_05_mod, outputs_05_nomod) + ) + assert all( + all(x == y for x, y in zip(a[0], b[0])) for a, b in zip(outputs_075_mod, outputs_075_nomod) + ) + assert all( + all(x == y for x, y in zip(a[0], b[0])) for a, b in zip(outputs_10_mod, outputs_10_nomod) + ) + + plot_specific_outputs(outputs_05_mod, outputs_05_nomod, primes, squaring_cost=0.5) + plot_specific_outputs(outputs_075_mod, outputs_075_nomod, primes, squaring_cost=0.75) + plot_specific_outputs(outputs_10_mod, outputs_10_nomod, primes, squaring_cost=1.0) diff --git a/oraqle/experiments/depth_aware_arithmetization/execution/poly_evaluation_pareto_front.py b/oraqle/experiments/depth_aware_arithmetization/execution/poly_evaluation_pareto_front.py new file mode 100644 index 0000000..8e43792 --- /dev/null +++ b/oraqle/experiments/depth_aware_arithmetization/execution/poly_evaluation_pareto_front.py @@ -0,0 +1,180 @@ +import math +import sys + +from galois import GF +from matplotlib import pyplot as plt +from matplotlib.ticker import MultipleLocator + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.abstract import SizeParetoFront +from oraqle.compiler.nodes.leafs import Input +from oraqle.compiler.polynomials.univariate import ( + UnivariatePoly, + _eval_poly, + _eval_poly_alternative, + _eval_poly_divide_conquer, +) + +if __name__ == "__main__": + sys.setrecursionlimit(15000) + + shape_size = 150 + + plt.figure(figsize=(3.5, 4.4)) + + marker1 = (3, 2, 0) + marker2 = (3, 2, 40) + marker3 = (3, 2, 80) + o_marker = "o" + linewidth = 2.5 + + squaring_cost = 1.0 + + p = 127 # 31 + gf = GF(p) + for d in [p - 1]: + x = Input("x", gf) + + poly = UnivariatePoly.from_function(x, gf, lambda x: x % 7) + coefficients = poly._coefficients + + # Generate points + print("Paterson & Stockmeyer") + depths = [] + sizes = [] + + front = SizeParetoFront() + + for k in range(1, len(coefficients)): + res, pows = _eval_poly(x, coefficients, k, gf, squaring_cost) + circ = Circuit([res]).arithmetize() + depths.append(circ.multiplicative_depth()) + sizes.append(circ.multiplicative_size()) + front.add(res, circ.multiplicative_depth(), circ.multiplicative_size()) # type: ignore + print(k, circ.multiplicative_depth(), circ.multiplicative_size()) + + data = {(d, s) for d, s in zip(depths, sizes)} + plt.scatter( + [d for d, _ in data], + [s for _, s in data], + marker=marker2, # type: ignore + zorder=10, + alpha=0.4, + s=shape_size, + linewidth=linewidth, + ) + + print("Baby-step giant-step") + depths2 = [] + sizes2 = [] + for k in range(1, len(coefficients)): + res, pows = _eval_poly_alternative(x, coefficients, k, gf) + circ = Circuit([res]).arithmetize() + depths2.append(circ.multiplicative_depth()) + sizes2.append(circ.multiplicative_size()) + front.add(res, circ.multiplicative_depth(), circ.multiplicative_size()) # type: ignore + + data2 = {(d, s) for d, s in zip(depths2, sizes2)} + plt.scatter( + [d for d, _ in data2], + [s for _, s in data2], + marker=marker1, # type: ignore + zorder=11, + alpha=0.45, + s=shape_size, + linewidth=linewidth, + ) + + print("Divide and conquer") + depths3 = [] + sizes3 = [] + for k in range(1, len(coefficients)): + res, pows = _eval_poly_divide_conquer(x, coefficients, k, gf, squaring_cost) + circ = Circuit([res]).arithmetize() + depths3.append(circ.multiplicative_depth()) + sizes3.append(circ.multiplicative_size()) + front.add(res, circ.multiplicative_depth(), circ.multiplicative_size()) # type: ignore + + data3 = {(d, s) for d, s in zip(depths3, sizes3)} + plt.scatter( + [d for d, _ in data3], + [s for _, s in data3], + marker=marker3, # type: ignore + zorder=11, + alpha=0.45, + s=shape_size, + linewidth=linewidth, + ) + + # Plot the front + front_initial = [(d, s) for d, s in data2 if d in front._nodes_by_depth and front._nodes_by_depth[d][0] == s] # type: ignore + front_advanced = [(d, s) for d, s in data if d in front._nodes_by_depth and front._nodes_by_depth[d][0] == s] # type: ignore + front_divconq = [(d, s) for d, s in data3 if d in front._nodes_by_depth and front._nodes_by_depth[d][0] == s] # type: ignore + + plt.scatter( + [d for d, _ in front_initial], + [s for _, s in front_initial], + marker=marker1, # type: ignore + zorder=10, + color="tab:orange", + s=shape_size, + label="Baby-step giant-step", + linewidth=linewidth, + ) + plt.scatter( + [d for d, _ in front_advanced], + [s for _, s in front_advanced], + marker=marker2, # type: ignore + zorder=10, + color="tab:blue", + s=shape_size, + label="Paterson & Stockmeyer", + linewidth=linewidth, + ) + plt.scatter( + [d for d, _ in front_divconq], + [s for _, s in front_divconq], + marker=marker3, # type: ignore + zorder=10, + color="tab:green", + s=shape_size, + label="Divide & Conquer", + linewidth=linewidth, + ) + + k = round(math.sqrt(d / 2)) + res, pows = _eval_poly(x, coefficients, k, gf, squaring_cost) + circ = Circuit([res]).arithmetize() + plt.scatter( + circ.multiplicative_depth(), + circ.multiplicative_size(), + marker=o_marker, + s=shape_size + 50, + facecolors="none", + edgecolors="black", + ) + plt.text( + circ.multiplicative_depth(), + circ.multiplicative_size() + 0.4, + f"k = {k}", + ha="center", + fontsize=8, + ) + + plt.xlim((5, 15)) + plt.ylim((15, 30)) + + plt.gca().set_aspect("equal") + + plt.gca().xaxis.set_minor_locator(MultipleLocator(1)) + plt.gca().yaxis.set_minor_locator(MultipleLocator(1)) + + plt.grid(True, which="both", zorder=1, alpha=0.5) + + plt.xlabel("Multiplicative depth") + plt.ylabel("Multiplicative size") + + plt.legend(fontsize="small") + + plt.savefig("poly_eval_front_2.pdf", bbox_inches="tight") + plt.show() diff --git a/oraqle/experiments/depth_aware_arithmetization/execution/run_all.sh b/oraqle/experiments/depth_aware_arithmetization/execution/run_all.sh new file mode 100755 index 0000000..8230af4 --- /dev/null +++ b/oraqle/experiments/depth_aware_arithmetization/execution/run_all.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Get the directory where the script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Change to the script's directory +cd "$SCRIPT_DIR" + +# Loop through all Python files in the script's directory +for file in *.py +do + # Check if there are any Python files + if [ -e "$file" ]; then + echo "Running $file" + python3 "$file" + else + echo "No Python files found in the script's directory." + break + fi +done diff --git a/oraqle/experiments/depth_aware_arithmetization/execution/veto_voting_per_mod.py b/oraqle/experiments/depth_aware_arithmetization/execution/veto_voting_per_mod.py new file mode 100644 index 0000000..238cc22 --- /dev/null +++ b/oraqle/experiments/depth_aware_arithmetization/execution/veto_voting_per_mod.py @@ -0,0 +1,99 @@ +from typing import List + +from galois import GF +from matplotlib import pyplot as plt +from sympy import sieve + +from oraqle.compiler.boolean.bool_and import _minimum_cost +from oraqle.compiler.boolean.bool_or import Or +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.abstract import CostParetoFront, UnoverloadedWrapper +from oraqle.compiler.nodes.leafs import Input +from oraqle.experiments.oraqle_spotlight.experiments.veto_voting_minimal_cost import ( + exponentiation_results, +) + + +def generate_all_fronts(): + results = {} + + for p in [7, 11, 13, 17]: + fronts = [] + + print(f"------ p = {p} ------") + for k in range(2, 51): + gf = GF(p) + xs = [Input(f"x{i}", gf) for i in range(k)] + + circuit = Circuit([Or(set(UnoverloadedWrapper(x) for x in xs), gf)]) + front = circuit.arithmetize_depth_aware(cost_of_squaring=1.0) + + print(f"{k}.", end=" ") + for f in front: + print(f[0], f[1], end=" ") + + print() + fronts.append(front) + + results[p] = fronts + + return results + + +def plot_fronts(fronts: List[CostParetoFront], color, label, **kwargs): + plt.scatter([], [], color=color, label=label, **kwargs) + for k, front in zip(range(2, 51), fronts): + for depth, cost, _ in front: + kwargs["marker"] = (depth, 2, 0) + kwargs["s"] = 16 + kwargs["linewidth"] = 0.5 + plt.scatter(k, cost, color=color, **kwargs) + + +if __name__ == "__main__": + fronts_by_p = generate_all_fronts() + max_k = 50 + + plt.figure(figsize=(4, 4)) + + plt.plot( + range(2, max_k + 1), + [k - 1 for k in range(2, max_k + 1)], + color="gray", + linestyle="solid", + label="Naive", + linewidth=0.7, + ) + + plot_fronts(fronts_by_p[7], "tab:purple", "Modulus p = 7", zorder=100) + plot_fronts(fronts_by_p[13], "tab:green", "Modulus p = 13", zorder=100) + + best_costs = [100000000.0] * (max_k + 1) + best_ps = [None] * (max_k + 1) + # This is for sqr = 0.75 mul + primes = list(sieve.primerange(300))[1:50] + for p in primes: + for k in range(2, max_k + 1): + cost = _minimum_cost(k, exponentiation_results[p][0][0][1], p) + if cost < best_costs[k - 2]: + best_costs[k - 2] = cost + best_ps[k - 2] = p + + plt.step( + range(2, max_k + 1), + best_costs[:-2], + zorder=10, + color="gray", + where="mid", + label="Lowest for any p", + linestyle="solid", + linewidth=0.7, + ) + + plt.legend() + + plt.xlabel("Number of operands") + plt.ylabel("Multiplicative size") + + plt.savefig("veto_voting.pdf", bbox_inches="tight") + plt.show() diff --git a/oraqle/experiments/oraqle_spotlight/examples/and_16.py b/oraqle/experiments/oraqle_spotlight/examples/and_16.py new file mode 100644 index 0000000..d1efb56 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/examples/and_16.py @@ -0,0 +1,17 @@ +from galois import GF + +from oraqle.compiler.boolean.bool_and import all_ +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input + +if __name__ == "__main__": + gf = GF(17) + + xs = (Input(f"x{i + 1}", gf) for i in range(16)) + + conjunction = all_(*xs) + + circuit = Circuit([conjunction]) + arithmetic_circuit = circuit.arithmetize() + + arithmetic_circuit.to_pdf("conjunction.pdf") diff --git a/oraqle/experiments/oraqle_spotlight/examples/common_expressions.py b/oraqle/experiments/oraqle_spotlight/examples/common_expressions.py new file mode 100644 index 0000000..0942584 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/examples/common_expressions.py @@ -0,0 +1,44 @@ +from typing import Tuple + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.abstract import Node +from oraqle.compiler.nodes.arbitrary_arithmetic import sum_ +from oraqle.compiler.nodes.leafs import Input + + +def generate_nodes() -> Tuple[Node, Node]: + gf = GF(31) + + x = Input("x", gf) + y = Input("y", gf) + z1 = Input("z1", gf) + z2 = Input("z2", gf) + z3 = Input("z3", gf) + z4 = Input("z4", gf) + + comparison = x < y + sum = sum_(z1, z2, z3, z4) + cse1 = comparison & sum + + comparison = y > x + sum = sum_(z3, z2, z4) + z1 + cse2 = sum & comparison + + return cse1, cse2 + + +def test_cse_equivalence(): + cse1, cse2 = generate_nodes() + assert cse1.is_equivalent(cse2) + + +if __name__ == "__main__": + cse1, cse2 = generate_nodes() + + cse1 = Circuit([cse1]) + cse2 = Circuit([cse2]) + + cse1.to_pdf("cse1.pdf") + cse2.to_pdf("cse2.pdf") diff --git a/oraqle/experiments/oraqle_spotlight/examples/equality_31.py b/oraqle/experiments/oraqle_spotlight/examples/equality_31.py new file mode 100644 index 0000000..a5cb051 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/examples/equality_31.py @@ -0,0 +1,18 @@ +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input + +if __name__ == "__main__": + gf = GF(31) + + x = Input("x", gf) + y = Input("y", gf) + + equality = x == y + + circuit = Circuit([equality]) + arithmetic_circuits = circuit.arithmetize_depth_aware(cost_of_squaring=1.0) + + for d, _, arithmetic_circuit in arithmetic_circuits: + arithmetic_circuit.to_pdf(f"equality_{d}.pdf") diff --git a/oraqle/experiments/oraqle_spotlight/examples/equality_and_comparison.py b/oraqle/experiments/oraqle_spotlight/examples/equality_and_comparison.py new file mode 100644 index 0000000..cb31753 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/examples/equality_and_comparison.py @@ -0,0 +1,19 @@ +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input + +if __name__ == "__main__": + gf = GF(31) + + x = Input("x", gf) + y = Input("y", gf) + z = Input("z", gf) + + comparison = x < y + equality = y == z + both = comparison & equality + + circuit = Circuit([both]) + + circuit.to_pdf("example.pdf") diff --git a/oraqle/experiments/oraqle_spotlight/examples/t2_comparison.py b/oraqle/experiments/oraqle_spotlight/examples/t2_comparison.py new file mode 100644 index 0000000..1f43ec8 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/examples/t2_comparison.py @@ -0,0 +1,21 @@ +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.leafs import Input + +p = 7 +gf = GF(p) + +x = Input("x", gf) +y = Input("y", gf) + +comparison = 0 + +for a in range((p + 1) // 2, p): + comparison += 1 - (x - y - a) ** (p - 1) + +circuit = Circuit([comparison]) # type: ignore + +if __name__ == "__main__": + circuit.to_graph("t2.dot") + circuit.to_pdf("t2.pdf") diff --git a/oraqle/experiments/oraqle_spotlight/experiments/comparisons/comparisons_bench.py b/oraqle/experiments/oraqle_spotlight/experiments/comparisons/comparisons_bench.py new file mode 100644 index 0000000..70a8889 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/experiments/comparisons/comparisons_bench.py @@ -0,0 +1,115 @@ +import random +import subprocess + +from galois import GF +from matplotlib import pyplot as plt +from sympy import sieve + +from oraqle.compiler.circuit import ArithmeticCircuit, Circuit +from oraqle.compiler.comparison.comparison import SemiStrictComparison, T2SemiLessThan +from oraqle.compiler.nodes.leafs import Input + + +def run_benchmark(arithmetic_circuit: ArithmeticCircuit) -> float: + # Prepare the benchmark + arithmetic_circuit.generate_code("main.cpp", iterations=10, measure_time=True) + subprocess.run("make", capture_output=True, check=True) + + # Run the benchmark + command = ["./main"] + p = arithmetic_circuit._gf.characteristic + command.append(f"x={random.randint(0, p - 1)}") + command.append(f"y={random.randint(0, p - 1)}") + print("Running:", " ".join(command)) + result = subprocess.run(command, capture_output=True, text=True, check=False) + + if result.returncode != 0: + print("stderr:") + print(result.stderr) + print() + print("stdout:") + print(result.stdout) + + # Check if the noise was not too large + print(result.stdout) + lines = result.stdout.splitlines() + for line in lines[:-1]: + assert line.endswith("1") + + run_time = float(lines[-1]) / 10 + print(p, run_time) + + return run_time + + +if __name__ == "__main__": + run_benchmarks = False + gen_plots = True + + if run_benchmarks: + primes = list(sieve.primerange(300))[2:20] + + our_times = [] + t2_times = [] + + for p in primes: + gf = GF(p) + + x = Input("x", gf) + y = Input("y", gf) + + print(f"-------- p = {p}: ---------") + our_circuit = Circuit([SemiStrictComparison(x, y, less_than=True, gf=gf)]) + our_front = our_circuit.arithmetize_depth_aware() + print("Our circuits:", our_front) + + ts = [] + for _, _, arithmetic_circuit in our_front: + ts.append(run_benchmark(arithmetic_circuit)) + our_times.append(tuple(ts)) + + t2_circuit = Circuit([T2SemiLessThan(x, y, gf)]) + t2_arithmetization = t2_circuit.arithmetize() + print( + "T2 circuit:", + t2_arithmetization.multiplicative_depth(), + t2_arithmetization.multiplicative_size(), + ) + + t2_times.append(run_benchmark(t2_arithmetization)) + + print(primes) + print(our_times) + print(t2_times) + + if gen_plots: + primes = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71] + our_times = [(0.0156603,), (0.0523416,), (0.0954489,), (0.0936497,), (0.111959,), (0.128402,), (0.288951,), (0.42076, 0.368583), (0.416362,), (0.40343,), (0.385652,), (0.437486,), (0.481356,), (0.522607, 0.504944), (0.526451,), (0.5904119999999999, 0.5146740000000001), (0.592896,), (0.621265, 0.598357)] + t2_times = [0.0156379, 0.0938689, 0.23473899999999998, 0.319668, 0.366707, 0.6632450000000001, 1.8380299999999998, 1.14859, 2.9022200000000002, 3.2060299999999997, 3.5419899999999997, 4.53918, 5.02624, 5.4439, 8.64118, 6.6267499999999995, 6.99609, 9.21295] + + plt.figure(figsize=(4, 2)) + plt.grid(axis="y", zorder=-1000, alpha=0.5) + + plt.scatter( + range(len(primes)), t2_times, marker="_", label="T2's Circuit", color="tab:orange" + ) + + for x, ts in enumerate(our_times): + for t in ts: + plt.scatter( + x, + t, + marker="_", + label="Oraqle's circuits" if x == 0 else None, + color="tab:cyan", + ) + + plt.xticks(range(len(primes)), primes, fontsize=8) # type: ignore + + plt.xlabel("Modulus") + plt.ylabel("Run time (s)") + + plt.legend() + + plt.savefig("t2_comparison.pdf", bbox_inches="tight") + plt.show() diff --git a/oraqle/experiments/oraqle_spotlight/experiments/large_equality/.gitignore b/oraqle/experiments/oraqle_spotlight/experiments/large_equality/.gitignore new file mode 100644 index 0000000..adf89f2 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/experiments/large_equality/.gitignore @@ -0,0 +1,6 @@ +/CMakeFiles +CMakeCache.txt +cmake_install.cmake +helib.log +Makefile +main diff --git a/oraqle/experiments/oraqle_spotlight/experiments/large_equality/CMakeLists.txt b/oraqle/experiments/oraqle_spotlight/experiments/large_equality/CMakeLists.txt new file mode 100644 index 0000000..a172dbd --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/experiments/large_equality/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(helib) +add_executable(main main.cpp) +target_link_libraries(main helib) diff --git a/oraqle/experiments/oraqle_spotlight/experiments/large_equality/large_equality.py b/oraqle/experiments/oraqle_spotlight/experiments/large_equality/large_equality.py new file mode 100644 index 0000000..e77a453 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/experiments/large_equality/large_equality.py @@ -0,0 +1,104 @@ +import math +import random +import subprocess +import time +from typing import List, Tuple + +from galois import GF +from sympy import sieve + +from oraqle.compiler.boolean.bool_and import all_ +from oraqle.compiler.circuit import ArithmeticCircuit, Circuit +from oraqle.compiler.nodes.leafs import Input + + +def generate_circuits(bits: int) -> List[Tuple[int, ArithmeticCircuit, int, float]]: + circuits = [] + + primes = list(sieve.primerange(300))[:10] # [:55] # p <= 257 + start = time.monotonic() + times = [] + for p in primes: + # (6, 63.0): p=2 + # (7, 58.0): p=5 + # (8, 51.0): p=17 + + limbs = math.ceil(bits / math.log2(p)) + + gf = GF(p) + + xs = [Input(f"x{i}", gf) for i in range(limbs)] + ys = [Input(f"y{i}", gf) for i in range(limbs)] + circuit = Circuit([all_(*(xs[i] == ys[i] for i in range(limbs)))]) + + inbetween = time.monotonic() + front = circuit.arithmetize_depth_aware(0.75) + + print(f"{p}.", end=" ") + + for f in front: + circuits.append((p, f[2], f[0], f[1])) + print(f[0], f[1], end=" ") + + inbetween_time = time.monotonic() - inbetween + print(inbetween_time) + times.append((p, inbetween_time)) + + print(times) + print("Total time", time.monotonic() - start) + + return circuits + + +if __name__ == "__main__": + bits = 64 + benchmark_circuits = False + generate_table = True + + # Run a benchmark for all circuits in the front + if benchmark_circuits: + # Generate all circuits per p + circuits = generate_circuits(bits) + + results = [] + for p, arithmetic_circuit, d, c in circuits: + # Prepare the benchmark + params = arithmetic_circuit.generate_code("main.cpp", iterations=10, measure_time=True) + subprocess.run("make", check=True) + + # Run the benchmark + command = ["./main"] + limbs = math.ceil(bits / math.log2(p)) + for i in range(limbs): + command.append(f"x{i}={random.randint(0, p - 1)}") + command.append(f"y{i}={random.randint(0, p - 1)}") + print("Running:", " ".join(command)) + result = subprocess.run(command, capture_output=True, text=True, check=False) + + if result.returncode != 0: + print("stderr:") + print(result.stderr) + print() + print("stdout:") + print(result.stdout) + + # Check if the noise was not too large + print(result.stdout) + lines = result.stdout.splitlines() + for line in lines[:-1]: + assert line.endswith("1") + + run_time = float(lines[-1]) / 10 + print(p, run_time, d, c, params) + results.append((p, d, c, params, run_time)) + + print(results) + + if generate_table: + gen_times = [(2, 0.007554411888122559), (3, 0.06264467351138592), (5, 8.457202550023794), (7, 0.05447225831449032), (11, 0.0478445328772068), (13, 0.052152080461382866), (17, 0.04349260404706001), (19, 0.04553743451833725), (23, 0.05198719538748264), (29, 0.046183058992028236)] + results = [(2, 6, 63.0, (16383, 1, 142, 3), 3.27577), (3, 7, 60.75, (32768, 1, 170, 3), 1.51993), (5, 7, 58.0, (32768, 1, 178, 3), 1.7679099999999999), (5, 8, 55.5, (32768, 1, 197, 3), 1.93994), (7, 8, 74.0, (32768, 1, 206, 3), 2.90913), (7, 9, 70.0, (32768, 1, 226, 3), 2.6624600000000003), (7, 10, 69.5, (32768, 1, 246, 3), 3.00814), (11, 9, 69.25, (32768, 1, 228, 3), 2.50603), (11, 12, 68.25, (32768, 1, 300, 3), 3.25469), (13, 9, 68.75, (32768, 1, 237, 3), 2.67845), (13, 10, 67.75, (32768, 1, 237, 3), 2.7718), (13, 11, 66.0, (32768, 1, 237, 3), 2.56386), (13, 12, 65.0, (32768, 1, 301, 3), 3.10959), (17, 8, 51.0, (32768, 1, 217, 3), 1.8792300000000002), (19, 9, 79.0, (32768, 1, 238, 3), 2.85011), (19, 10, 68.0, (32768, 1, 259, 3), 2.8636500000000003), (23, 9, 89.0, (32768, 1, 248, 3), 4.135730000000001), (23, 10, 80.0, (32768, 1, 270, 3), 3.75128), (29, 9, 83.0, (32768, 1, 249, 3), 3.7119), (29, 10, 75.0, (32768, 1, 271, 3), 3.46666)] + + gen_times = {p: t for p, t in gen_times} + + for p, d, c, params, run_time in results: + print(f"{p} & {d} & {c} & {params[0]} & {params[1]} & {params[2]} & {params[3]} & {round(gen_times[p], 2)} & {round(run_time, 2)} \\\\") diff --git a/oraqle/experiments/oraqle_spotlight/experiments/veto_voting_minimal_cost.py b/oraqle/experiments/oraqle_spotlight/experiments/veto_voting_minimal_cost.py new file mode 100644 index 0000000..04fd8e7 --- /dev/null +++ b/oraqle/experiments/oraqle_spotlight/experiments/veto_voting_minimal_cost.py @@ -0,0 +1,82 @@ +"""Finds the minimum cost for veto voting circuits for different prime moduli.""" + +from sympy import sieve + +from oraqle.compiler.boolean.bool_and import _minimum_cost + +exponentiation_results = { + 2: ([(0, 0.0)], 8.633400000002123e-05), + 3: ([(1, 0.75)], 4.6670000000137435e-06), + 5: ([(2, 1.5)], 7.695799999996034e-05), + 7: ([(3, 2.5)], 0.0053472920000000035), + 11: ([(4, 3.25)], 0.007671625000000015), + 13: ([(4, 3.25)], 0.002812749999999975), + 17: ([(4, 3.0)], 7.891700000001167e-05), + 19: ([(5, 4.0)], 0.012155541999999964), + 23: ([(5, 5.0)], 0.03937258299999996), + 29: ([(5, 5.0)], 0.018942542000000007), + 31: ([(5, 6.0), (6, 5.0)], 0.064326), + 37: ([(6, 4.75)], 0.019883207999999986), + 41: ([(6, 4.75)], 0.02284237499999997), + 43: ([(6, 5.75)], 0.03223737499999996), + 47: ([(6, 6.75), (7, 6.0)], 0.607119292), + 53: ([(6, 5.75)], 0.03940958299999997), + 59: ([(6, 6.75)], 1.243811584), + 61: ([(6, 6.75), (7, 5.75)], 0.446000167), + 67: ([(7, 5.5)], 0.051902208000000005), + 71: ([(7, 6.5)], 0.18221370799999997), + 73: ([(7, 5.5)], 0.044685417000000005), + 79: ([(7, 6.75)], 0.362901958), + 83: ([(7, 6.5)], 0.121000375), + 89: ([(7, 6.5)], 0.182695375), + 97: ([(7, 5.5)], 0.06858350000000002), + 101: ([(7, 6.5)], 0.38408749999999997), + 103: ([(7, 7.5), (8, 6.5)], 3.3626029170000002), + 107: ([(7, 7.5)], 8.891771667), + 109: ([(7, 7.5), (8, 6.5)], 4.596561917), + 113: ([(7, 6.5)], 0.1859389579999995), + 127: ([(7, 9.5), (8, 7.5)], 1619.89318625), + 131: ([(8, 6.25)], 0.05858354099996177), + 137: ([(8, 6.25)], 0.10623299999999991), + 139: ([(8, 7.25)], 1.2351711669999998), + 149: ([(8, 7.25)], 0.48292875), + 151: ([(8, 7.5)], 4.641820375), + 157: ([(8, 7.5)], 2.49218775), + 163: ([(8, 7.25)], 0.5001321249999999), + 167: ([(8, 8.25), (9, 7.5)], 48.444338791), + 173: ([(8, 8.25), (9, 7.5)], 37.677076833), + 179: ([(8, 8.25)], 132.232723375), + 181: ([(8, 8.25), (9, 7.25)], 53.822612083999985), + 191: ([(8, 9.25), (9, 8.25)], 907.7980847910001), + 193: ([(8, 6.25)], 0.12370429100008096), + 197: ([(8, 7.25)], 0.6496936670000002), + 199: ([(8, 8.25), (9, 7.25)], 50.102889333), + 211: ([(8, 8.25)], 83.20584475), + 223: ([(8, 10.0), (9, 8.25)], 6772.927301542), + 227: ([(8, 8.25)], 50.801469917), + 229: ([(8, 8.25)], 39.942074416000004), +} + + +def run_experiments(): + """Run the experiments and prints the results.""" + max_k = 50 + best_costs = [100000000.0] * (max_k + 1) + best_ps = [None] * (max_k + 1) + + # This is for sqr = 0.75 mul + primes = list(sieve.primerange(300))[1:50] + for p in primes: + print(f"------ p = {p} ------") + for k in range(2, max_k + 1): + cost = _minimum_cost(k, exponentiation_results[p][0][0][1], p) + if cost < best_costs[k - 2]: + best_costs[k - 2] = cost + best_ps[k - 2] = p + + for k, cost, p in zip(range(2, max_k + 1), best_costs, best_ps): + print(k, cost, p) + + +if __name__ == "__main__": + run_experiments() diff --git a/oraqle/helib_template/.gitignore b/oraqle/helib_template/.gitignore new file mode 100644 index 0000000..adf89f2 --- /dev/null +++ b/oraqle/helib_template/.gitignore @@ -0,0 +1,6 @@ +/CMakeFiles +CMakeCache.txt +cmake_install.cmake +helib.log +Makefile +main diff --git a/oraqle/helib_template/CMakeLists.txt b/oraqle/helib_template/CMakeLists.txt new file mode 100644 index 0000000..a172dbd --- /dev/null +++ b/oraqle/helib_template/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.10.2 FATAL_ERROR) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(helib) +add_executable(main main.cpp) +target_link_libraries(main helib) diff --git a/oraqle/helib_template/main.cpp b/oraqle/helib_template/main.cpp new file mode 100644 index 0000000..ad77e16 --- /dev/null +++ b/oraqle/helib_template/main.cpp @@ -0,0 +1,83 @@ + +#include +#include +#include + +#include + +typedef helib::Ptxt ptxt_t; +typedef helib::Ctxt ctxt_t; + +std::map input_map; + +void parse_arguments(int argc, char* argv[]) { + for (int i = 1; i < argc; ++i) { + std::string argument(argv[i]); + size_t pos = argument.find('='); + if (pos != std::string::npos) { + std::string key = argument.substr(0, pos); + int value = std::stoi(argument.substr(pos + 1)); + input_map[key] = value; + } + } +} + +int extract_input(const std::string& name) { + if (input_map.find(name) != input_map.end()) { + return input_map[name]; + } else { + std::cerr << "Error: " << name << " not found" << std::endl; + return -1; + } +} + +int main(int argc, char* argv[]) { + // Parse the inputs + parse_arguments(argc, argv); + + // Set up the HE parameters + unsigned long p = 5; + unsigned long m = 8192; + unsigned long r = 1; + unsigned long bits = 72; + unsigned long c = 3; + helib::Context context = helib::ContextBuilder() + .m(m) + .p(p) + .r(r) + .bits(bits) + .c(c) + .build(); + + + // Generate keys + helib::SecKey secret_key(context); + secret_key.GenSecKey(); + helib::addSome1DMatrices(secret_key); + const helib::PubKey& public_key = secret_key; + + // Encrypt the inputs + std::vector vec_x(1, extract_input("x")); + ptxt_t ptxt_x(context, vec_x); + ctxt_t ciph_x(public_key); + public_key.Encrypt(ciph_x, ptxt_x); + std::vector vec_y(1, extract_input("y")); + ptxt_t ptxt_y(context, vec_y); + ctxt_t ciph_y(public_key); + public_key.Encrypt(ciph_y, ptxt_y); + + // Perform the actual circuit + ctxt_t stack_0 = ciph_x; + ctxt_t stack_1 = ciph_y; + stack_1 *= 4l; + stack_0 += stack_1; + stack_0 *= stack_0; + stack_0 *= stack_0; + stack_0 *= 4l; + stack_0 += 1l; + ptxt_t decrypted(context); + secret_key.Decrypt(decrypted, stack_0); + std::cout << decrypted << std::endl; + + return 0; +} diff --git a/pyproject.toml b/pyproject.toml index 9f69c2c..a29cc77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "oraqle" -description = "Secure computation compiler for MPC, FHE, and arithmetic circuits in general" -version = "0.0.1" +description = "Secure computation compiler for homomorphic encryption and arithmetic circuits in general" +version = "0.1.0" requires-python = ">= 3.8" authors = [ {name = "Jelle Vos", email = "J.V.Vos@tudelft.nl"}, @@ -10,16 +10,3 @@ maintainers = [ {name = "Jelle Vos", email = "J.V.Vos@tudelft.nl"} ] readme = "README.md" - -[tool.isort] -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true -line_length = 100 -profile = "black" -skip_gitignore = true - -[tool.black] -line_length = 100 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f7aedc4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +sympy +six +galois>=0.3.8 +aeskeyschedule +python-sat +git+https://github.com/jellevos/fhegen.git +matplotlib diff --git a/requirements_dev.txt b/requirements_dev.txt index e8f9035..c901bea 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,9 @@ -flake8 pytest -black -isort -pep8-naming +gensafeprime +graphviz +tabulate +ruff +mkdocs +mkdocstrings[python] +mkautodoc +pymdown-extensions diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..1eb6ad8 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,55 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".ipynb_checkpoints", + ".mypy_cache", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +line-length = 100 +indent-width = 4 +target-version = "py38" + +[lint] +preview = true +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["W", "E4", "E7", "E9", "F", "ERA001", "B", "D", "DOC", "PLW", "B", "SIM", "UP", "PLR", "RUF", "PIE"] +ignore = ["E203", "E501", "E731", "D105", "W293", "PLR2004", "PLR6301"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[lint.per-file-ignores] +"oraqle/experiments/*" = ["D", "DOC"] + +[lint.pydocstyle] +# Use Google-style docstrings. +convention = "google" + +[format] +preview = true +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = true +docstring-code-line-length = "dynamic" diff --git a/setup.cfg b/setup.cfg index fe6d2d8..ad0bae2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,3 @@ -[flake8] -max-line-length = 100 -extend-ignore = E203, E501, W503, E731 -exclude = venv build -per-file-ignores = __init__.py:F401 - [tool:pytest] python_files = *.py norecursedirs = venv build diff --git a/tests/test_circuit_sizes_costs.py b/tests/test_circuit_sizes_costs.py new file mode 100644 index 0000000..7699f42 --- /dev/null +++ b/tests/test_circuit_sizes_costs.py @@ -0,0 +1,93 @@ +"""Test file for testing circuits sizes.""" + +from collections import Counter + +from galois import GF + +from oraqle.compiler.nodes.abstract import ArithmeticNode, UnoverloadedWrapper +from oraqle.compiler.nodes.arbitrary_arithmetic import Sum +from oraqle.compiler.nodes.leafs import Constant, Input + + +def test_size_exponentiation_chain(): + """Test.""" + gf = GF(101) + + x = Input("x", gf) + + x = x.mul(x, flatten=False) + x = x.mul(x, flatten=False) + x = x.mul(x, flatten=False) + + x = x.to_arithmetic() + assert isinstance(x, ArithmeticNode) + assert ( + x.multiplicative_size() == 3 + ), f"((x^2)^2)^2 should be 3 multiplications, but counted {x.multiplicative_size()}" + assert x.multiplicative_cost(0.5) == 1.5 + + +def test_size_sum_of_products(): + """Test.""" + gf = GF(101) + + a = Input("a", gf) + b = Input("b", gf) + c = Input("c", gf) + d = Input("d", gf) + + ab = a * b + cd = c * d + + out = ab + cd + out = out.to_arithmetic() + + assert isinstance(out, ArithmeticNode) + assert ( + out.multiplicative_size() == 2 + ), f"a * b + c * d should be 2 multiplications, but counted {out.multiplicative_size()}" + assert out.multiplicative_cost(0.7) == 2 + + +def test_size_linear_function(): + """Test.""" + gf = GF(101) + + a = Input("a", gf) + b = Input("b", gf) + c = Input("c", gf) + + out = Sum( + Counter({UnoverloadedWrapper(a): 1, UnoverloadedWrapper(b): 3, UnoverloadedWrapper(c): 1}), + gf, + gf(2), + ) + + out = out.to_arithmetic() + assert out.multiplicative_size() == 0 + assert out.multiplicative_cost(0.5) == 0 + + +def test_size_duplicate_nodes(): + """Test.""" + gf = GF(101) + + x = Input("x", gf) + + add1 = x.add(Constant(gf(1))) + add2 = x.add(Constant(gf(1))) + + mul1 = x.mul(x, flatten=False) + mul2 = x.mul(x, flatten=False) + + add3 = mul2.add(add2, flatten=False) + + mul3 = mul1.mul(add3, flatten=False) + + out = add1.add(mul3, flatten=False) + + out = out.to_arithmetic() + + assert isinstance(out, ArithmeticNode) + assert out.multiplicative_size() == 3 + assert out.multiplicative_cost(0.7) == 2.4 diff --git a/tests/test_poly2circuit.py b/tests/test_poly2circuit.py new file mode 100644 index 0000000..5b18db8 --- /dev/null +++ b/tests/test_poly2circuit.py @@ -0,0 +1,63 @@ +"""Test file for generating circuits using polynomial interpolation.""" + +import itertools + +from oraqle.compiler.func2poly import interpolate_polynomial +from oraqle.compiler.poly2circuit import construct_circuit + + +def _construct_and_test_circuit_from_bivariate_lambda(function, modulus: int, cse=False): + poly = interpolate_polynomial(function, modulus, ["x", "y"]) + circuit, gf = construct_circuit([poly], modulus) + circuit = circuit.arithmetize() + + if cse: + circuit.eliminate_subexpressions() + + for x, y in itertools.product(range(modulus), repeat=2): + print(function, x, y) + assert circuit.evaluate({"x": gf(x), "y": gf(y)}) == [function(x, y)] + + +def test_inequality_mod7(): + """Tests x != y (mod 7).""" + _construct_and_test_circuit_from_bivariate_lambda(lambda x, y: int(x != y), modulus=7) + + +def test_inequality_mod13(): + """Tests x != y (mod 13).""" + _construct_and_test_circuit_from_bivariate_lambda(lambda x, y: int(x != y), modulus=13) + + +def test_max_mod7(): + """Tests max(x, y) (mod 7).""" + _construct_and_test_circuit_from_bivariate_lambda(max, modulus=7) + + +def test_max_mod13(): + """Tests max(x, y) (mod 13).""" + _construct_and_test_circuit_from_bivariate_lambda(max, modulus=13) + + +def test_xor_mod11(): + """Tests x ^ y (mod 11).""" + _construct_and_test_circuit_from_bivariate_lambda(lambda x, y: (x ^ y) % 11, modulus=11) + + +def test_inequality_mod11_cse(): + """Tests x ^ y (mod 11) with CSE.""" + _construct_and_test_circuit_from_bivariate_lambda( + lambda x, y: int(x != y), modulus=11, cse=True + ) + + +def test_max_mod7_cse(): + """Tests max(x, y) (mod 7) with CSE.""" + _construct_and_test_circuit_from_bivariate_lambda(max, modulus=7, cse=True) + + +def test_xor_mod13_cse(): + """Tests x ^ y (mod 13) with CSE.""" + _construct_and_test_circuit_from_bivariate_lambda( + lambda x, y: (x ^ y) % 13, modulus=13, cse=True + ) diff --git a/tests/test_sugar_expressions.py b/tests/test_sugar_expressions.py new file mode 100644 index 0000000..f778d0b --- /dev/null +++ b/tests/test_sugar_expressions.py @@ -0,0 +1,22 @@ +"""Test file for sugar expressions.""" + +from galois import GF + +from oraqle.compiler.circuit import Circuit +from oraqle.compiler.nodes.arbitrary_arithmetic import sum_ +from oraqle.compiler.nodes.leafs import Input + + +def test_sum(): + """Tests the sum_ function.""" + gf = GF(127) + + a = Input("a", gf) + b = Input("b", gf) + + arithmetic_circuit = Circuit([sum_(a, 4, b, 3)]).arithmetize() + + for val_a in range(127): + for val_b in range(127): + expected = gf(val_a) + gf(val_b) + gf(7) + assert arithmetic_circuit.evaluate({"a": gf(val_a), "b": gf(val_b)}) == expected