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
+
+
+
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": "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"
+ },
+ "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": "iVBORw0KGgoAAAANSUhEUgAAANYAAAWFCAYAAAB1wSnQAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nOzdeVzN2f8H8Ne9dUurFlkayR5ZByP70jSKREIhNS2TaCYyaMYwlhnrMLZBhoTSihaMbMkklEGRVLaomFJab/tdfn/41o/Rcut+Pvdzb/c8H495PGbq3nPeGa/O557P+ZzDEgqFQhAEQSk20wUQRFtEgkUQNCDBIggaKDJdAEGIqra2FpmZmSgtLUVxcTG4XC4AQF1dHVpaWtDU1ESPHj3A4XAYrpQEi5BSPB4P9+7dQ2xsLBISEvD48WO8fPkStbW1Tb6Pw+Gge/fuMDY2xujRozF58mQMHz4cCgoKEqr8PRaZFSSkBZ/Px5UrVxAQEIDz58+jtLQUnTp1wqhRo2BkZITevXujd+/e0NTURPv27aGqqgoAqKioQElJCUpKSvD8+XM8e/YMGRkZuH37Nt6+fQtNTU1YWVnBwcEBZmZmEgkZCRbBuMLCQuzbtw+HDx9Gbm4uRo4cCWtra0yYMAF9+/YVq+2MjAzcuHEDERERuHPnDvT19eHu7g5PT09oa2tT9BN8igSLYExBQQG2b9+OQ4cOQUlJCU5OTpg3bx569OhBS38vXrxASEgIjh8/Dh6PBw8PD3h7e0NHR4fyvkiwCIkTCAQ4evQoVq9eDQUFBXz77bf4+uuvoaamJpH+uVwujh8/Dh8fHwgEAmzfvh3Ozs5gsViU9UGCRUjU8+fP4eDggH/++Qdubm7w9vaGuro6I7WUlZVh27Zt8PPzg4mJCfz9/dGzZ09K2ib3sQiJCQsLw7Bhw1BeXo6YmBj88ssvjIUKADQ0NLB582ZcvXoVpaWlGDZsGM6cOUNJ2yRYBO0EAgGWL18OOzs7zJ07F9HR0TA2Nma6rHoDBgxAdHQ0bGxsMGfOHKxcuRICgUCsNsmloIQIBAK8fPkSr169QkVFBSoqKqCoqAgNDQ107NgRffr0gYqKCtNlUq6mpgZff/01IiIisG/fPtjY2DBdUpPOnDmDZcuWYfbs2Th27BiUlJRa1Q4JFk1qampw9epVxMTE4Pr1v5Ga+gjV1dWNvp7NZqNbN0OMHz8OpqamsLS0hJ6engQrpl5VVRVmzpyJW7duwd/fH+PHj2e6JJHExcXh66+/xtixYxEVFQVlZeUWt0GCRbGMjAzs378fwcEhKCx8h95GAzFs9GT0HzQchr36Qd+gB9qpqEJFVQ08Xi0qy7koePsvXr3IwPOMR7ifcB0P796CQMCHuYUF3BctgqWlJaUzVpLA5/Nha2uLmJgYnDlzBkOGDGG6pBZ58OABZs+eDTMzM4SGhrb4pjIJFkXS09Oxbt06nDlzBl0Ne2H6XGdMnbUQnfQNWtxWVWUFrl+KwIUz/ki8cQUDBw7C+vXrMHv2bBoqp8fixYvh7++PsLAwjBo1iulyWuX27duwtbWFk5MTfHx8WvReEiwxVVZWYtOmTdi5cye69+4PZ8+1mGxhAzabmnmhZ+kpOH5gC66cC8VXX03BgQP70bt3b0rapouPjw88PT1x/PhxWFhYMF2OWP766y+4uLjg4MGDcHd3F/l9JFhiSEtLw9y5tsjKzob7yk2Ys3AJ2DStQ3tw9yZ2rPXA6+wXOOTjg4ULF9LSj7hSUlJgYmICT09PrFq1iulyKLF161YcPHgQt27dwueffy7Se0iwWikqKgoL7O3Ru99g/PpHMLp8Zkh7nzxeLQ5sW40g313w8PDA3r17Jb5quykVFRUYOnQoOnfujFOnTklVbeLg8/mwsbHBu3fvkJSUJNLsLbmP1Qp+fn6YM2cOLKwX4lDY3xIJFQAoKnKwbO1ObDt0Gr6+R2FrZ9fkTKOkbdmyBbm5uTh48GCbCRUAKCgowMfHB69fv8a2bdtEeg8JVgsFBgbCzc0Njkt+xI9bDkFRUfIP1U22sMG+gEu4cuUq7BcuBJ/Pl3gN//X06VPs3LkTq1evRufOnZkuh3L6+vrw9vbG9u3bkZGR0ezryaVgC1y5cgWWlpaY5+IFz59+Y7ocJCXGYamDOdzcvsEff/zBaC3Tp09HdnY2Ll++3KZGqw/xeDyYmZmhd+/eiIyMbPK1JFgiysnJwdChn+OLCebYuDtAau4rXYs+g9VL5uLkyZNYsGABIzU8ePAAn3/+OYKDg/Hll18yUoOkXLp0CQ4ODkhOTsbgwYMbfR0JlgiEQiEmTZ6M7DdvcfzsP1BRlczjDaLa8+v3OBvqiwfJyZStzm4JW1tbpKenIyYmRmp+4dBFKBTCzMwMxsbGCA4ObvR15DOWCPz8/HDz5k38sjdQ6kIFAN/+uA2d9Q3xnaenxPt+9eoVzpw5Ay8vrzYfKgBgsVj47rvvcOrUKWRnZzf6OhKsZhQXF8Pb+wfMdfwWRgNEu4chaRyOElb9egAXo6Nx9uxZifYdEBAAHR0dTJ06VaL9MsnS0hJaWloIDAxs9DUkWM04cOAAavl8LPp+I9OlNOlzkwmYPNUG69dvgCSv7gMDA2FjYyMVW45JipKSEqytreHv79/oa0iwmlBZWYk9e/diruN3UNdoz3Q5zXLxXIsHD5Jx8eJFifSXnJyM9PR0zJkzRyL9SZO5c+ciLS0NKSkpDX6fBKsJERERKC4uhp2z5D+7tEZf46H4YqwpDh85IpH+YmJioKOjI3Mr16nw+eefQ1dXFzExMQ1+nwSrCcePn8CYSVOhrduR6VJENnWWAy789RcKCgpo7ys2Nhbjxo2jbMGxLGGz2Rg9ejRiY2Mb/r6E65EZZWVliI29hq+s5jFdSotMnjobQry/30InoVCIGzduYOzYsbT2I83Gjh2LuLi4Bj/TkmA14saNG+Dz+fhirCkl7WU+fYzfNyzD11ZfUNJeY1TV1DFwqEmjv0mpkp2djdLSUgwaNIi2PnJycrBs2TLweLxGX5OXl4dTp05hz549ePnyZatf0xoDBw5EcXEx3rx588n3SLAacfPmTfTs0x86HTpR0t6b7Ezc/vsiigvpv0QbNmoS4uJu0NpH3Xq5Xr160dK+QCDAd999h6CgoEY3dvH394ezszN69uyJZcuWoXv37q16TWvVPRfX0NpBEqxGpKWloUffgZS1N9bUEv0GDqOsvab0MhqIzMwXtK58z8jIgI6ODi27yALvH5Z89+5dg98TCoVwdHREZGQkIiIiMHz48E9uTovyGnF16NABWlpaSE9P/+R7JFiNSM94gm49xNs3/L8ktRLesJcReDweXrx4QVsfeXl5tK1if/z4MR4+fNjoVgQHDhzA3bt3cejQoUY3ehHlNVTo3Lkz3r59+8nXSbAaUZCfD92OFP/FYbHqf2vevn4RB7avxtXzYdT2AdRfvtI5M1hWVkbLZps1NTXYsGEDtm7d2uD3Hz58iC1btsDDwwMdOzY8WyvKa6iirq6O0tLST75OzsdqRBm3DKqq1P/FEQqF8NmxBv/cvIa8f7Nx4uA23Lh6Dhv3BFDWh6qaBoD3f/npwuVyaQnWpk2b4OHh0egl5qFDhyAUCmFoaAhPT09kZWVhyJAhWLlyJTQ1NUV+DVXU1dUb/HMmI1YjamtqoMhp3WaNTSkpeoeJU6zhF3kbETeeY+Q4M0RHnETijSuU9aGk9P7Sp6qqirI2/6u6upryZUxxcXEAgEmTJjX6mvv376NDhw4QCATYtm0bPDw8cOzYMcyYMaN+9lCU11BFWVm5wT9nEqxGqKqpoaqynPJ2tXQ6wHjI+yl3JSVlzFqwCACQGHeZsj4q/1c3nfuiq6mpoaKigrL2iouLcfDgQaxdu7bR15SUlODFixcYP348Zs6cCTU1NZibm8PFxQWpqakIDw8X6TVU4nK50NDQ+OTrJFiN0FDXQDmXvkupOiYTpkBBURH5eZ/eC2mt8rL31/wN/Q+nioaGRv0ZwFTYtGkTWCwWfv31V/z888/4+eefceXK+1F8w4YNCA4ORklJCYRC4SeXiSYmJgCAR48eifQaKpWXlzf450w+YzXCwMAA/+a8pL0fdY32UFZWQbcefShr8012JgDA0JC+TW40NTUp/Qynra2NzMxMPH78uP5rdbNtaWlpaN++PebNmwd1dXXk5uZ+9N4vvnh/BaCqqgoDA4NmX0Ol0tLSBj+3kWA1ol8/Izx50fymIeJ6l5+LivIyfG4ygbI2X73IgLqGBrp06UJZm/9laGiI7Oxs8Pl8Sva4WLNmzSdf27NnDzZv3ozQ0ND6wwlGjx79yYry169f13+PxWI1+xqq8Pl85OTkNHjTmVwKNmLIkCFIT7lH+bNNVZUVqKr8/88mAX/uwPS5ThgxhpqlUwCQ9vAuBg0cROsTvf369UN1dXX9X1hJ2bZtG96+fYvTp0/Xf+3KlSuYNGkSJk6cKPJrqJCVlYWamhr069fvk++REasRkyZNwvfff4/n6Sno3b/xTUNawsbeHVkvnsBh2jCYWy/AvzkvodFeGz9sOkhJ+3Xu3boGx4X0Lh42MjICADx58gTdunWjta8PdevWDX/++Sc2btyIf//9F7m5uSgsLPzooUNRXkOFp0+fgsVioU+fTy/jyWYyjRAIBOjYsRPmfbMCX3v8SGnb7/Jz8fbfHHTv3Z/yPTSyM59i9qS+uHbtGiZPnkxp2//Vq1cvzJo1Cz/+SO2fjyhqamqQmZkJAwODRj83ifIacWzZsgXnz5/HkydPPvkeuRRsBJvNhp2dLaIjqLtxW0dXrzP6Dx5By8Y0F8ID0KWLPiZMoO4zW2MmT56Mmzdv0t5PQ5SUlGBkZNRkYER5jTji4+NhatrwJTwJVhMcHR3x4sljpNy/zXQpIuHzeIiOCIC9/QKJbJppamqK+/fvU3o/S1ZwuVwkJyc3elVAgtUEExMTjB4zBv4+25kuRSQXIwORn/saHh4eEunPzMwMAoEAly9Td3NbVtQ9SNrYBqUkWM1Y/eOPiLtyFumP7jNdSpNqa2tw/MAWLFiwAD169JBInx07dsSUKVMQFkb9QmJpFxYWBgsLC3To0KHB75NgNWP69OkYM3Ysdqz1EPskdToF++7G29wcbNiwQaL9Ojo6IjY2Fvn5+RLtl0l5eXn4+++/4eDg0OhrSLCawWKx4HPwINJS7uG0/wGmy2lQduZTHN33K9b89BOlT8iKYsaMGdDS0oKvr69E+2WSr68vdHR0YGVl1ehrSLBEMGjQIPz888/Yt3mV1F0S1lRX4advbWHcvz8jJyiqqKhg2bJl8PX1RUlJicT7l7TS0lIcO3YMy5cvR7t27Rp9HQmWiNauXYtx48Zh9ZI5KHj7L9PlAHj/bNfmH93wb04mwsL+f9mPpH333XdgsVg4duwYI/1L0uHDh8Fms/Htt982+ToSLBGx2WyEhoZAXVUZXo4WKCspYrok7N20AjHnw3D61ClGThmpo6WlheXLl2Pv3r3491/p+KVDh9evX2P//v0iPTBJVl60UFZWFsaOHQdVDW3sPhENvU76Eq9BIBBg53pPRAT+iYCAAMyfP1/iNfxXdXU1Bg8eDGNjYxyR0E68kubs7Iz09HSkpKQ0eRkIkBGrxbp164abN+PBEtTAbfZYpKfck2j/ZaXF+MHdBufC/BAWFiYVoQLeP0m7Z88eREZGtsn7WtHR0Th//jwOHDjQbKgAMmK12rt372BnNw83btzAd6u3w9bJk/atllPu38Z6r4Xg11QhLCwU48aNo7W/1nB2dsbZs2dx7do1fPbZZ0yXQ4mcnByYmprCxsZG5NlPEiwxCAQCbNmyBb/88gv6Gg/Fyl/2Y8DQkZT3U/QuHz6//YSzYX746qsp8Pc/QfvuQ61VXl6OkSNHQkNDAxERETJ/vE9tbS1mzpyJyspKJCYmirzukFwKioHNZmPt2rVISkqCrpYqXKxH4Xvn6Uj+J56S9vNzX2PvppWwHtcDiX9fQGBgIC5ejJbaUAHv98IIDQ1FamoqvLy8JHpWF9WEQiGWLVuG9PR0hIWFtWgxLxmxKBQdHY1fN23C7Vu30L2XESxmLcSYSVPR13go2CIuis19k4U78Vdx5WwI7t66ho4dO2HlyhVwd3eHmpr0HdPamJiYGFhaWsLBwaHRPQKl3fr163HkyBGcO3cO5ubmLXovCRYN7t69i4CAAISGhiEvLxeaWtowGvA5DHr0RWf9btDU0oGKqhqqKitQWc7Fu/xcZGU+wfOMFOS8egHldu1gYmICblkZ/v77b1p3W6JTYGAgHBwc4OXlhdWrV8vMGcVCoRBbtmzBvn37cPLkyVZNEJFg0UgoFCI1NRWxsbFISUlBxpMnyM7KRlFREcrLuVBTU4e6ujo6dOiAfv2M0L9/f4wfPx6jR49Gfn4+evTogdOnT8Pa2prpH6XVjh07hkWLFmHevHnYuXOnRB5nEQePx8OKFSsQFhaGI0eOwMnJqVXtkGBJMXNzcygpKeHcuXNMlyKWc+fOwc7ODmPHjsWBAwdoO0hBXIWFhViyZAkSEhIQGhqK6dOnt7otMnkhxVxdXREdHY3s7GymSxGLlZUVrl27hidPnsDU1BR37txhuqRPJCYmYvLkyXj+/DliY2PFChVAgiXVrK2toaOjgxMnTjBdithGjRqF+/fvY9iwYZg5cyY2bNiA8nLqdxpuKS6Xi3Xr1sHa2hpffPEF7t+/j5Ejxb9lQoIlxZSUlODg4AA/Pz+pfhZMVLq6uoiKisL+/fsRFBSEMWPGICoqipEpeYFAgPDwcIwZMwahoaE4ePAgIiIiKLtMJcGScm5ubnj58iWuXbvGdCmUYLFYcHd3R0ZGBqZMmYJFixZh4sSJOHPmDPh8Pu3983g8nD59GhMmTMCSJUswdepUZGRkwM3NjdJZSzJ5IQPGjh0LAwMDhISEMF0K5VJTU7F161aEhIRAX18ftra2mDt3LuVHsD579gxhYWE4deoUcnNzMX/+fKxevRr9+/entJ86JFgywM/PD0uWLEFOTg709PSYLocWz58/x1dffYXi4mIUFRVhyJAhmDBhAsaPHw8TE5MWb2FWXl6OxMRE3LhxA3FxcXj48CG6du0Ke3t7LFq0iPbHbEiwZEB5eTn09fWxYcMGLF++nOlyaPHw4UMMGTIEMTEx4PP5iIqKQmxsLB4/fgw2mw0DAwP06tULPXv2hI6ODtTV1etXopSXl4PL5aKwsBDPnz/H8+fPkZOTA4FAAGNjY5iammLmzJkwNTWlfaF0HRIsGeHu7o64uDikpaUxXQotPDw8cO3aNaSlpX30WSc3Nxe3bt1CRkYGnjx5gidPnqCoqAhcLrf+tBMNDQ2oq6tDW1sbffv2hZGREYyMjDB69GjazkluDgmWjLhz5w5MTExw69YtSk/MkAZcLhefffZZmxqRyaygjBg5ciSGDh2Ko0ePMl0K5YKDg1FdXY2FCxcyXQplSLBkiIuLC0JCQho8pV2W/fnnn7C1tW1TEzMkWDLEwcEBQqEQoaGhTJdCmbt37+LevXtwd3dnuhRKkc9YMmbhwoV49uwZEhISmC6FEm5ubrh58yZSU1Nl5rESUZARS8a4uroiMTERDx48YLoUsZWVlSE0NBQeHh5tKlQACZbMmTRpEvr06dMmNscMCAgAj8eDvb0906VQjgRLxrBYLDg7OyMgIABVVVVMlyMWX19fzJ8/H9ra2kyXQjkSLBnk7OyMsrIyREREMF1Kq92+fRtJSUltbtKiDpm8kFHW1tYoKytDTEwM06W0ipOTE5KTk5GcnMx0KbQgI5aMcnV1RWxsLJ49e8Z0KS1WXFyMU6dOYcmSJUyXQhsSLBk1bdo0dO3aVSYnMU6cOAE2my0122PTgQRLRikoKMDR0RF+fn6ora1lupwWOXLkCOzt7Zs9sUOWkWDJMFdXV7x9+xbR0dFMlyKyuLg4pKamYtGiRUyXQisyeSHjzMzMoKamhqioKKZLEYm9vT2ePXuGxMREpkuhFRmxZNw333yDCxcu4M2bN0yX0qx3794hPDy8zU6xf4gES8bZ2NhAW1sbx48fZ7qUZh07dgzKysqws7NjuhTakWDJOCUlJdjb28PX11eqt0gTCoU4cuQIHBwcZOpwh9YiwWoDXF1dkZmZievXrzNdSqPqdsJ1dXVluhSJIJMXbcTo0aPRs2dPBAYGMl1Kg2xtbfHmzRvEx1Nzdpi0IyNWG+Hq6oozZ86goKCA6VI+kZeXh6ioKLmYtKhDgtVGzJs3D0pKSlI5Yvn5+UFVVRWzZ89muhSJIcFqI9TV1WFnZyfy4dOSIhQK4efnB2dn5xZvuinLSLDaEFdXVzx69Eiqbr5eunQJz549wzfffMN0KRJFJi/amKFDh2LkyJE4fPgw06UAAGbNmoXi4mLExsYyXYpEkRGrjXFyckJwcHD9LrFM+vfff/HXX3/J1aRFHRKsNsbBwQE8Hg9hYWFMl4IjR46gffv2mDVrFtOlSBwJVhujq6uLWbNmMb5jLp/Ph5+fH1xcXKCsrMxoLUwgwWqDXF1dcfv2bTx8+JCxGi5cuICsrCy5m7SoQyYv2iChUIi+ffvCysoKu3bt+uh7AoEALBaL9n38pk+fjpqaGly+fJnWfqQVGbHaIBaLBScnJ/j7+6O6uhoAkJOTg02bNqFXr17gcrmU9RUXFwcLCwtERESAx+MBALKzs3Hx4kW5nLSoQ0asNio3NxcGBgZYtmwZHj9+jEuXLgF4P2Ll5uaiU6dOlPRz/vx5WFlZgcVioUOHDli8eHH9ZjFZWVngcDiU9CNrFJkugKDe06dPERgYCAUFBfz+++/gcDgfPVJC5UafZWVlYLFYEAqFyM/Px/bt21FTUwNDQ0NERkZi1qxZUFSUv79m5FKwjaisrMTJkycxbtw4GBkZYcuWLfWXgf/dbKaiooKyfrlcLhQUFOr/u6amBgDw+vVr2NraQl9fHz/++COysrIo61MWkGC1EcXFxVi9ejVu3boFoVDY5M5NlZWVlPVbXl7e4Lm+dZ+36kaxmTNn1odOHpBgtRFdunTBpUuXoKam1uwB1lQGi8vlNjnDqKCgAD09PZw9exZKSkqU9SvtSLDaEGNjY5w+fbrZ11EdrMaw2WwoKSnh8uXLMDAwoKxPWUCC1caYm5s3uwCX6mA1ttcGi8VCREQEhg4dSll/soIEqw1ydXXFihUrGrwkZLFYlE5elJWVNRgsFouFI0eOwNzcnLK+ZAkJVhv122+/wcrK6pOpbjabTemIVVZWBj6f/0kf69evh7OzM2X9yBoSrDaKzWYjODgYQ4cO/egmLdXBKikp+ei/FRQUYGdnh3Xr1lHWhywiwWrDVFRUcOHCBXTp0qV+5GKxWJQGq7S0tP7fFRUVMWbMGBw/frzNnSncUiRYbZyenh6io6PRrl07sNlsWj5jAQCHw0GvXr3kblq9MSRYcsDY2Bjh4eFgsViorq6mZbpdV1cX165dg5aWFmVtyzISLDnx1VdfwcfHBwC10+0VFRVQU1PDlStXoK+vT1m7sk7+VkfKMTc3Nzx79uyjz0XiEgqFOHv2LAYOHEhZm22B3Dw2wuPxkJmZiaysLJSVldWvpdPS0oKuri769u0LdXV1hqukF4/HQ3p6OkJDQ9GvXz+Ul5e3ui0lJSVoamri0aNHWL58OTQ0NCisVPa12WBVV1fj8uXLiImJQWzsdaSlPW72SFEDg24YP34cJk+ejOnTp6Nz584SqpY+tbW1iIiIQFBQEK5evSpWmBrDZrMxYsQIzJ49G05OTujYsSPlfciaNhes1NRU7N+/HyEhoSgtLUFf4yEYNnoy+g8aAcOefdH5M0Ooa7aHouL7eztlJUUoepePVy8y8DzjEe4nXMeDf+JRU1ONKebmWOTmhhkzZjS7sFUahYWFYeXKlXjz5g1GjRqFCRMmYNCgQTA0NKRkgxc+n4+8vDykp6fj9u3biImJQXV1Nby8vLBmzRq5HsXaTLBSU1Ox9uefcTYqCoY9+2K6rQssrO2h16nlH6hrqqsQd+Us/jpzArevX0S/fv2xfv062Nra0lA59dLS0rB48WLEx8fDysoKbm5uEhl9q6urERkZicOHD0NFRQW7du3C/Pnzae9XGsl8sCoqKrBhwwbs2bMHvfsNgovnzxj/FXUjTObTxzh+cCsuRQZh0qTJ8PE5iL59+1LSNh2io6NhZ2cHQ0NDeHt7w9jYWOI1lJSUwMfHB2fOnIGXlxd27Njx0cOQ8kCmg/Xo0SPMnWuLN//mwsN7C6wXLKLtku1RUiJ+W7sEWZlPcGD/fjg5OdHSjzj2798PLy8vTJs2DT/99BPj+01cvnwZGzduhJmZGUJDQ+XiJMc6Mhus8PBw2C9ciH4Dh2PTH8Ho2KUr7X3yeTz47FyLgEO/YdGiRThw4IDU/CY+ceIEnJ2d4eHhIVWLX1NSUrBixQqMGTMGkZGRUvPnRTeZDNaRI0ewZMkS2Ngvxvfr90BBwpuV/H05CuuWLsAU8ykICQ5Gu3btJNr/f8XHx8PMzAz29vbw8PBgtJaGPH78GO7u7vDw8MDOnTuZLkciZC5Y/v7+cHJywjde6+HmtZ6xOh7cvYnvXabjS9PJOH3qFGO/ifPz89G/f38MHToUW7duldrZy0uXLmHt2rUICQmRmUkgcchUsC5evAgrKyssdF8FD+8tTJeD5H/isXThFDg7O+HgwYOM1ODu7o6oqCicOnVK6g9227x5MxITE5GRkdHmb8ZL56+3BmRnZ8N+4UKYz1yAJas2M10OAGDoF+Pw674gHDp0CAEBARLvPykpCUePHsXSpUulPlQA4OnpiYqKCmzdupXpUmgnEyOWQCDAhIkT8e/bQhw/+w/aqUjXX6J9m1chIugQkpOS0Lt3b4n1a2lpidevX8PX11dmnn8KCgrCwYMHkZOTA11dXabLoY1MjFi+vr5ISEjAr/uCpC5UAODxwxboG/TEt99+J7E+s7KycPHiRdjb28tMqADA2toaHA4H/v7+TJdCK6kPVlFREX5cvRp2zkvRp/8QpstpkKIiB6t+PYArVy4jIiJCIn1GRUVBTU0NEyZMkEh/VFFVVcXEiYoyd/UAACAASURBVBMRHh7OdCm0kvpg7d+/H3y+kNEZQFEM/WIcvpw2Bxs3/gJJXF3HxsZixIgRMrkv+ujRo5GQkEDpHvLSRqqDVVFRgb1798HWyRNq6ppMl9MsZ881ePjwAS5cuEB7Xw8ePICRkRHt/dDByMgIPB4PaWlpTJdCG6kOVnh4OEpKSzD3a8l9dhFHn/5DMHKcGY74+tLeV25ursw+nlFXd25uLsOV0Eeqg3XihD/GTp4GbV09pksR2VQbB0RfuICCggJa+6msrISKigqtfdClrm4qD8CTNlIbrNLSUly/HouvrOYxXUqLTDKfBYCFixcv0tqPDNwlaVTdLKYs/wzNkdpg3bhxA3w+HyPGTGa6lBZRVVPHwM9NEBsby3QpBIOkNlg3b95Ezz79odOBmiM9JWnYqEm4cSOe6TIIBkltsNLS0tCjr2zu/NOz7wC8ePG8/kRFQv5IbbAyMp7AsKdsTicb9jICn8/H8+fPmS5FZMnJyfDz8/vk67m5udi2bRsDFck2qQ1Wfn4+dPSovwzMznyKEL+9OLJnI25dj6a8fQDQ1Xu/vwTdM4NUGjp0KAoLC3H06NH6r+Xm5mLt2rVYsGABg5XJJqkNFrecC1VVah8t2Ll+KX71dsXUWQsxePgYLHeyhL/Pdkr7AABVtfe7E9Xtay4rVq5ciaKiIhw9erQ+VOvWrUO3bt2YLk3mSG2weLW1UORQu7n+hTP+GD3BHO21dWEy/it0790f1y9FUtoHAHD+V7csfsaq2y5t0aJFJFRikNpgqaiqoqqS2s0ldx//C7MdlgAAUpPvQCgUorqKun3M61RUvL/xKYv76uXm5uLly5cYNmwYrl69ynQ5Mktqg6WhroHyMur2GAeAISPG4n7i31jv5YCszCfQ79odQlB/k7Kc+75uWXtKNjc3F2vWrMG6deuwYcMGFBYWNjihQTRPaoPVrVs3vMl5SWmbf2zxxtlQP6zZfgRTZy0Eh4LdYBvyJisTANC9e3da2qfDh6EyNDQE8P6ykISrdaQ2WP36GSHrRQZl7aWn3EPAnzsw1/FbKCl/sKsSDctqXr3IgLqGhkzt/S4QCD4KVZ2VK1eiX79+DFUlu6Q2WEOGDEHGo/sNnsjeGnVPHv99ORJ8Hg934q/i6eMHKC0pQnbmU7zJzqSkHwBIe3gXgwcNlqkne/X19T8JVZ0xY8ZIuBrZJ7XBmjRpEooKC/A8I4WS9rr37o9pNg6IDD4CS5OuyHn1HDPtXFGQ9wYRQYehb9CDkn4A4N7ta/jyS1PK2iNkj9Q+fjp48GB06KCH+Ji/KHskf8Nufyxb+zs02mvVnzYyx9EDGu21KWkfeH8ZmP3yOb788kvK2iRkj9SOWGw2G3Z2trhw5gSl7Wrr6tWHCgCloQKA6PCT6NJFH+PGjaO03f9SVFSk7DJZ0vh8PgDI5LYCopLaYAGAo6MjXr14ggd3bzJdikh4vFpEh/tj4UJ72nfG1dTUlLmVHXXq6m7LB4FLdbBGjhyJMWPHIoCGZUd0iA4/iYK3/0pk//RevXohKyuL9n7o8OrVKwCQ6B6MkibVwQKAn1avxo2Y80h7eJfpUppUW1uDEwe3YuHChRK5fzV8+HCkpFAzsSNpKSkp0NHRgYGBAdOl0EbqgzVt2jSMGz8ev631kOrPFIGHf0d+3musXy+ZbdosLCyQmpqK/Px8ifRHpbi4OEydOlWmbke0lNQHi8ViwefgQTx5nIzQY/uYLqdBr15k4Ngfm/Dz2rWN3guimrm5ObS1tREVFSWR/qjy8uVLJCUlwd7enulSaCX1wQKAAQMGYP369Tiw7UepuySsrqrETx62GDhwIFasWCGxftu1a4fFixcjODgYJSUlEutXXH/++Sf69OkDc3NzpkuhlUwcigC8X3Jjbm6Bx+lPcOTMTeh1/ozpkt4vA1pmjzs3LiHp/n2Jrw0sKyuDkZERxo0bhx9++EGifbdGcnIy3NzccP78eUybNo3pcmglEyMW8P6+VmhoCNprqmHZ1xYoLS5kuiTs/mU5/r4UgTOnTzOy4FZDQwPbtm1DeHg4Hj16JPH+W6Kqqgrbtm3D1KlT23yoABkaserk5ORg7Nhx4LRTx76AS4yMXAI+H9vXeuBcmB8CAwMZPaFQKBTC0tISd+/exfHjx9Gpk/TtaiUQCLB69WokJyfjn3/+QY8e1C0fk1YyM2LV6dq1K27ejIeyIvCNzRikJt+RaP9lJUVY5WaN6IgAnDlzhvFjP1ksFkJCQtCxY0d8//33KC+n9uFQKhw4cAA3btxARESEXIQKkMFgAe/DdeNGHAYPMsaiueMReOR3CP63TIZOyXduwGHa53iRnoxrMTGYMWMG7X2KQlNTE+fPn0dJSQnc3NykZk90Pp+P7du3IyAgAEeOHMH48eOZLkliZDJYAKCjo4PoCxfwy8aN8PntJzjN+AIp92/T0ldhQR5+WekMd9uJ+HzoICQnJ2H06NG09NVa3bt3R0JCApSUlODk5ISHDx8yWk9paSm8vLxw/vx5nDp1Co6OjozWI2ky9xmrIenp6fj22+9w7VoMRk80h6PHjxhmMlHsG5C5b7IQdGQXooKPQEdHB3v27MacOXMoqpoeZWVlmDdvHi5duoSZM2fCw8NDomvyBAIBIiMj4ePjAxUVFZw9exbDhw+XWP/Sok0Eq86VK1fw66ZNuBEXB4PuvWBhvRCjJ1mg/6ARUBBxJfXrrBe4E38VV8+F4F7C3+jSRR/e3qvg5uYmM6d7CIVCBAUFwdvbG1wuF3Z2dpg5cyatTzRXVVUhJiYGQUFBePHiBTw9PbFu3Tq0b9+etj6lWZsKVp3k5GT4+/sjNDQMb968hrqGJvoaD0W3nkbo0rU71DQ0weEoQSgUgltajMJ3b/HqeTpeZDzCv6+zoKKqiqFDhqCmpga3b98Gh8NpvlMpxOVysWPHDhw6dAj5+fkwMjLCgAEDYGhoiHbt2jXfQDN4PB7evn2LjIwMJCUlQSAQwNraGhs3bpT7x/nbZLA+lJ6ejuvXr+Phw4dIT89AVlYWSktLUVNTAxaLBW1tbejq6qJfPyP0798f48ePh4mJCe7evYuxY8fi3r17GDZsGNM/hlhqampw7do1REdH4969e0hNTUV1dTUqK1u/9RuHw0H79u3RpUsXDB48GKamprCysoKenuycZUanNh8scRgbG2Py5Mk4cOAA06VQ5vr165g8eTKSk5MxZIh0HpbeFsjsrKAkODs7IzAwEBUVFUyXQpmjR49i5MiRJFQ0I8FqgqOjIyoqKhAZSf021EwoKSlBeHg4XF1dmS6lzSPBakKnTp1gaWn50QkcsiwoKAhCoZDx1SLygASrGa6uroiNjZWps64a4+fnh7lz57bpvSakBQlWMywsLNClSxecOEHtblGSlpKSgrt375LLQAkhwWqGoqIiHB0dcezYsfptu2SRr68vevbsKVfr9ZhEgiUCNzc3vH79GleuXGG6lFapqalBUFAQvvnmmza9z4Q0IcESQd1velmdxIiMjERxcTG+/vprpkuRGyRYInJxccHZs2dlcleko0ePwsLCAvr6+kyXIjdIsERka2sLVVVVBAYGMl1Ki+Tk5CAmJoZMWkgYCZaIVFRUYGtrK3OXg0ePHoWuri4sLS2ZLkWukGC1gKurKx49eoQ7dyS7HUBrCYVCBAQEwNHRUWZX6MsqEqwWGDlyJAYPHiwzR4fGxMTg+fPncHZ2ZroUuUOC1UJOTk4IDg6WiYW5R48exZgxY2BsbMx0KXKHBKuFHB0dUV1djdOnTzNdSpOKi4sRFRUFFxcXpkuRSyRYLaSrqwsrKyupn8Q4efIk2Gw2WXDLEBKsVnBxcUFcXBzS09OZLqVRfn5+sLOzg4aGBtOlyCUSrFYwNzdHt27d4O/vz3QpDbp//z6SkpLIvSsGkWC1ApvNrl+Yy+PxmC7nE35+fujbt6/U7X0oT0iwWsnFxQVv377FxYsXmS7lI1VVVWTBrRQgwWqlHj16YNKkSVJ3Tys8PBxlZWVYuHAh06XINRIsMbi4uODcuXNSs1c68P7e1fTp09GlSxemS5FrJFhimD17NjQ0NHDy5EmmSwHw/hjS69evk3tXUoAESwzt2rXD/Pnz4evrC2nYntHPzw8dO3aEhYUF06XIPRIsMbm4uCAjIwO3b9Nz0omoBAIBTpw4AScnJ7LgVgqQYIlp+PDhGDp0KOOTGJcvX0ZWVhZ5SlhKkGBRwMXFBaGhoSgrK2OsBj8/P0yYMEHuDyOQFiRYFLC3twePx8OpU6cY6f/du3c4e/YsmbSQIiRYFNDR0YG1tTVjC3MDAgLA4XAwe/ZsRvonPkWCRREXFxfcunULaWlp9V/LzMzE+vXrERMTQ0kffD4fixYtQkJCwkdfP378OBYsWAB1dXVK+iHER47xoYhQKETv3r0xc+ZMjBgxAocPH0ZcXByEQiFOnDhByRm85eXl9eHp27cv3N3dMWDAAFhYWCAhIQEmJiZi90FQQ7TzQ4lmJScno0OHDti3bx+EQiFYLBaEQiEUFRVRVVVFSR8ftvP06VN4e3tDKBRCQ0MDBQUF4PP5UFBQoKQvQjzkUlAMJSUlOHz4MEaOHIlhw4YhKSkJfD4fAoGgfjtqNptNWbA+PIFRKBTW91VRUYHp06ejU6dO+PHHH/Hs2TNK+iNajwSrlXg8Hr788ku4u7vj7t27AIDa2tpPXsdisWgZsT5UF+J3795hx44dMDY2RmpqKiV9Eq1DgtVKioqKOH36NHR0dMBmN/3HSHewPiQQCHDo0CEMGDCAkj6J1iHBEkP37t1x6dIlKCoqNvnsU3V1NSX9NRcsNpuNH3/8kdzPkgIkWGIaMWIEAgICGv2+UCiUyIilqKgIKysrbN68mZK+CPGQYFFg7ty52LhxY4OjliSCxeFw0L9/fwQGBjZ7WUpIBvm/QJG1a9diwYIFn0x3UxmsD2cF6ygqKkJLSwsXLlyAmpoaJf0Q4iPBogiLxcKxY8cwZsyYjx7bEAgEtI1YbDYbHA4HV65cQdeuXSnpg6AGCRaFOBwOIiIi8Nlnn0FR8f29d4FA0OBI0xpVVVWfXOqFhIRgyJAhlLRPUIcEi2K6urq4ePEiVFRU6kNA1T7vHwaLxWJhz549mDFjBiVtE9QiwaKBkZERoqKi6iczysvLKWm3uroaAoEACgoK8PDwgKenJyXtEtQjwaLJ5MmTcfjwYQANTzq0RlVVFQQCAb788kvs3buXkjYJepBFuDRycXHBkydPcOnSJUraq6qqwsCBA3Hq1Cmy2FbKkWDRiMfjwcHBAa9fv0ZgYKBYl4RKSkp49eoVNm3aRHa4lQHkeSyK1dbWIiIiAkFBQbh69Spln68+xGazMWLECMyePRtOTk7o2LEj5X0Q4iHBolBYWBhWrlyJN2/eYNSoUZgwYQIGDRoEQ0NDKCsri90+n89HXl4e0tPTcfv2bcTExKC6uhpeXl5Ys2YNObJHipBgUSAtLQ2LFy9GfHw8rKys4Obmhs6dO9Peb3V1NSIjI3H48GGoqKhg165dmD9/Pu39Es0jwRJTdHQ07OzsYGhoCG9vb0bO+y0pKYGPjw/OnDkDLy8v7Nixg0xuMIwESwz79++Hl5cXpk2bhp9++onxHWgvX76MjRs3wszMDKGhoWTtIINIsFrpxIkTcHZ2hoeHB5ydnZkup15KSgpWrFiBMWPGIDIykoxcDCHBaoX4+HiYmZnB3t4eHh4eTJfzicePH8Pd3R0eHh7YuXMn0+XIJRKsFsrPz0f//v0xdOhQbN26VWqff7p06RLWrl2LkJAQ2NraMl2O3CHBaiF3d3dERUXh1KlTUFVVZbqcJm3evBmJiYnIyMggm3lKmHT+upVSSUlJOHr0KJYuXSr1oQIAT09PVFRUYOvWrUyXInfIiNUClpaWeP36NXx9fWVmWVFQUBAOHjyInJwc6OrqMl2O3CAjloiysrJw8eJF2Nvby0yoAMDa2hocDgf+/v5MlyJXSLBEFBUVBTU1NUyYMIHpUlpEVVUVEydORHh4ONOlyBUSLBHFxsZixIgR9Y/cy5LRo0cjISGBsr03iOaRYInowYMHMDIyYrqMVjEyMgKPx/voiCGCXiRYIsrNzZXZxzPq6s7NzWW4EvlBgiWiyspKqKioMF1Gq9TVzeVyGa5EfpBgiUiW70rUzWLK8s8ga0iwCIIGJFhSQCAQoLS0lOkyCAqRYEmBvLw8/PHHH0yXQVCIBIsgaECCRRA0IMEiCBrI3vqcNuDs2bNIT0+v/+/y8nKkpaXht99+++h1rq6uZEW6jCLBYsCoUaMwcODA+v8uKChAVVUV5syZ89HrNDU1JV0aQRESLAZ07Njxo+VRKioq0NTURM+ePRmsiqAS+YxFEDQgwSIIGpBgSQFlZWX06tWL6TIICpFgSQEdHR3MmzeP6TIICpFgEQQNSLBEpKioCIFAwHQZrcLn8wFAJrcVkFUkWCLS1NREWVkZ02W0Sl3dWlpaDFciP0iwRNSrVy9kZWUxXUarvHr1CgDQu3dvhiuRHyRYIho+fDhSUlKYLqNVUlJSoKOjAwMDA6ZLkRskWCKysLBAamoq8vPzmS6lxeLi4jB16lSZ2mhU1pFgicjc3Bza2tqIiopiupQWefnyJZKSkmBvb890KXKFBEtE7dq1w+LFixEcHIySkhKmyxHZn3/+iT59+sDc3JzpUuQKCVYL/PDDD1BVVcWhQ4eYLkUkycnJuHr1Knbv3i2153i1VeRPuwU0NDSwbds2hIeH49GjR0yX06Sqqips27YNU6dOxbRp05guR+6QY3xaSCgUwtLSEnfv3sXx48fRqVMnpkv6hEAgwOrVq5GcnIx//vkHPXr0YLokuUOC1QqlpaUYM2YMeDweDh8+LHWn0//xxx8IDg7G1atXMX78eKbLkUvkUrAVNDU1cf78eZSUlMDNzU1q9kTn8/nYvn07AgICcOTIERIqBpFgtVL37t2RkJAAJSUlODk54eHDh4zWU1paCi8vL5w/fx6nTp2Co6Mjo/XIO3IpKKaysjLMmzcPly5dwsyZM+Hh4SHRNXkCgQCRkZHw8fGBiooKzp49i+HDh0usf6JhJFgUEAqFCAoKgre3N7hcLuzs7DBz5kx07tyZtj6rqqoQExODoKAgvHjxAp6enli3bh3at29PW5+E6EiwKMTlcrFjxw4cOnQI+fn5MDIywoABA2BoaIh27dqJ3T6Px8Pbt2+RkZGBpKQkCAQCWFtbY+PGjejXrx8FPwFBFRIsGtTU1ODatWuIjo7GvXv3kJmZiXfv3qG6urrVbSoqKkJTUxOfffYZBg8eDFNTU1hZWUFPT4/CygmqkGBJiJqaGg4cOAAnJ6cWv5fL5UJDQwN//fUXudkrI8isoASUl5ejoqKi1aOLuro6VFRUZHJlvbwiwZKAukCIc9nWoUMHFBQUUFUSQTMSLAmoC5Y4h4Pr6emREUuGkGBJABUjFgmWbCHBkoD8/HyoqqqKtaaQBEu2kGBJwNu3b8WeFifBki0kWBJQVFQEHR0dsdrQ1dVFYWEhRRURdCPBkoC6+1DiUFNTQ3l5OUUVEXQjwZIALpcr9jNb6urq4HK5FFVE0I0ESwK4XC7U1dXFaoMES7aQYEkAFSOWmpoa+Hw+KisrKaqKoBMJlgSUl5dTMmIBIKOWjCDBkgCqLgXr2iKkHwmWBFB1KQiAzAzKCBIsCaisrISKiopYbZBgyRYSLAmora0Fh8MRq426Q+N4PB4VJRE0I8GSAD6fDwUFBbHaqHt/3emMhHQjwZIAHo8n9jGlZMSSLSRYEkBlsMiIJRtIsCSAyktBMmLJBhIsCSCXgvKHBEsCyKWg/CHBkgCBQCD2wW/kUlC2kGBJAJvNhkAgEKuNupFK3M9qhGSQYEmAoqKi2CNN3fvFvaQkJIMESwI4HA5qa2vFaqPu/eKu4CAkgwRLAsiIJX9IsCRAUVFR7BGLBEu2kGBJAIfDISOWnCHBkoAPLwWFQiEKCwvx4sULFBcXN/qejIwM5OXl1R/9U/d+8hlLNpBjfCgWGBiIBw8eoKioCIWFhSgoKMCdO3egrKwMPp//0RPAaWlpjR4YN3jwYKSkpAAAlJWVoa6ujqKiovqD7HR0dKCtrQ1tbW14e3uL/bwXQTEhQam9e/cKAQgVFBSEABr9p0uXLk22s2rVqibbUFBQELLZbOHo0aMl9JMRLUEuBSnm7OwMVVXVJpcecTgcWFlZNdnOlClTmmyDz+dDKBTCy8ur1bUS9CHBopiGhgacnZ2b/CzE4/Fgbm7eZDvjx49v9tziDh06YNasWa2qk6AXCRYNli1b1uQsIJvNhqmpaZNtKCsrY8KECY2uMeRwOFi6dCmZzJBSJFg06NOnDyZNmtTg1DiLxcLw4cOhpaXVbDvTpk1rNFhCoRBubm5i10rQgwSLJsuXL29w1OJwOJg+fbpIbZibmzfahp2dHTp16iR2nQQ9yHQ7TQQCAbp3747s7OxPvnfnzh188cUXIrXTtWtXvH79+pOvJyYmYuTIkWLXSdCDjFg0YbPZWLZs2SeXg5qamhg+fLjI7VhaWn70OYrNZmPYsGEkVFKOBItGrq6uHwVLUVERFhYWLXrosaHLwe+//56yGgl6kGDRSEtLCw4ODvUjjlAoxNSpU1vUhpmZ2UdBbN++PebMmUNpnQT1SLBo9uHUO5/Ph5mZWYveX3fpyGKxwOFw4OnpCWVlZTpKJShEgkWzAQMGYOzYsQAAIyMjdO3atcVtTJ8+HUKhEAKBAO7u7lSXSNCABEsCli9fDgAiT7P/V90qDRsbG+jr61NWF0EfEiwJmDFjBrp169bsMqbGjBgxArq6uli6dCnFlRF0IU/N0YjH4+HJkyfIzMzExIkT6/+9NUxNTZGfn4/ExEQYGxtDQ0OD4moJKpEbxBSrra1FREQEgoOCcPXqVXBpOM+KzWbji+EjYDNnNpycnNCxY0fK+yDEQ4JFobCwMKxauRKvX7/BlOFfYIbJGIzqNwBGXQ2goiT+TB6Pz0d2/lskPX+Ki/cScfpmHCqqq+C1fDnWrFlDRjEpQoJFgbS0NCxZvBg34uPh9NVUrF/wNbrp0b+Or7KmGr4Xz2ND0Akoq6jg9927MH/+fNr7JZpHgiWm6OhozLOzQ7/PDLB/8TJ80bfhR+3p9K60FGsDfPHnhbPw8vLCjh07yI65DCPBEsP+/fvh5eUFB9Mp+NNzBZQUmX02KjTuGpx2bYPZV2YICQ0V+0BxovVIsFrpxIkTcHZ2xuavv8Fq24VMl1MvIf0xZvzyE0aNG4uIyEgycjGEBKsV4uPjYfbll1gxyxabv5a+hw3vPs3AxB+WYsm332Lnzp1MlyOXSLBaKD8/H8b9+2Ni/4EIW70BbJZ03mMPvh4D+x2/IiQkBLa2tkyXI3dIsFrI3d0df0VEIv2QP9SlfC+/Rft24sLDe0jPyIC6ujrT5cgV6fx1K6WSkpJw9OhR7HBeLPWhAoBtzotQVV6OrVu3Ml2K3CEjVgtMt7RE0atsxP/2B1gsFtPliGR3RBjWBBxFdk4OdHV1mS5HbpARS0RZWVmIvngRK6xtZSZUAOBmYQUlRUX4+/szXYpcIcESUVRUFDRUVWFlMobpUlpEXUUF1qPGISI8nOlS5AoJlohir12D6eBh4MjgMTrmw0bidkICqqqqmC5FbpBgiejhgwf4vFdvpstolc979QGPx0NaWhrTpcgNEiwR5ebloWsH2Xw8o2sHPQBAbm4uw5XIDxIsEVVUVkKtmUMKpFVd3R+ezUXQiwRLRLJ8V6JuFlOWfwZZQ4JFEDQgwSIIGpBgEQQNSLAYEp+ags0hAZ98PSs/Dx4HdjFQEUElEiyGjBswCG9LirAp+P+XGmXl58H+t1+xfBZ5zEPWkWAxaK/7UuSXFmNTsH99qPyW/4g++i3fhpqQLiRYDNvrvhQv3+ZiovdSEqo2hASLYVn5eUjPzsLEQUNx6sZ1psshKEKCxaCs/Dws2P4rjnp54/j3q5FXXIgtoSeZLougAAkWQz4MlVHXbgDeXxaScLUNJFgMEQiEH4Wqzl73pfi8Vx+GqiKoInsPF7UR3Tt1bvR7U0eYSLASgg5kxCIIGpBgEQQNSLBEpKioCD5fwHQZrcLj8wG8/xkIySDBElF7TU0Ul8vmg4J1dWtpaTFcifwgwRJRr5698OR1NtNltEpGThYAoHdv2dyzQxaRYIlo2IjhuJ3xmOkyWiUh/TF0dXRgYGDAdClygwRLRBYWFvgnIw1v3hUwXUqLRSXehMXUqTK10aisI8ESkbm5OXS0tXH08l9Ml9Ii6dlZiH/0EPb29kyXIldIsETUrl07uC9ejD1RZ/CutJTpckS27qQf+vbuA3Nzc6ZLkSskWC3www8/oJ2aKtad9GO6FJHEp6bgdPx17NqzG2w2+V8tSeRPuwU0NDSwdds2/HnhLBKlfCKjoroKSw7uxrSpUzFt2jSmy5E75BifFhIKhZhuaYmkO/8gcZcPDPSkb3dcgVAA260b8HfaI9z55x/06NGD6ZLkDhmxWojFYiE4JAS6nTvB6pefUFpRznRJn/jp+BGcS7yF8IgIEiqGkGC1gqamJs6dP4+35WUY770UWfl5TJcE4P3SpW8P7sGOMyE44uuL8ePHM12S3CLBaqXu3bvjdkIChKrtYPL9EtxOS2W0niJuGaZvXI3jMRdx6tQpODo6MlqPvCPBEoOhoSFu3rqF4SYmGL/KE+5//I6C0hKJ1sAXCPDnhbPou2ghHr3JRtyNG7CxsZFoDcSnyOQFBYRCIYKCgvCDtzcqyrjwtJoFV3NLdNPrRFufFdVVOB3/N3ZFnsLjVy/hudQT69atQ/v27WnrkxAdCRaFuFwuduzYgT8PHcLb/HwM7d0HJn36w6irAVSVxT8CqJbPQ05BPpJePENcSjL4AgGsra2x8Zdf0K9fPwp+AoIqJFgUurEE0AAAIABJREFUSklJgbGxMfh8Pq5du4bo6Gjcv3cPmS8y8a7wHaqqq1vdtqKiItprauIzfX0MGjIEpqamsLKygp6eHoU/AUEZIUGJd+/eCbW1tYW7d+9u8PuqqqrCY8eOtartsrIyIQDhX3/9JUaFhCSRyQuKbNq0CYqKinBxcfnke+Xl5aioqGj16KKurg4VFRXk5+eLWyYhIeRZbQpkZmbi4MGD2L17NzQ1NT/5fl0gxLls69ChAwoKZO+RFXlFRiwKrF69Gt27d8c333zT4PfrgtWxY+uXP+np6ZERS4aQEUtMd+7cQVhYGCIiIsDhcBp8DRUjFgmWbCEjlphWr16NUaNGYcaMGY2+Jj8/H6qqqlBTU2t1PyRYsoWMWGKIiopCbGws4uPjm3zs/e3bt2JPi+vp6eHZs2ditUFIDhmxWonP5+Onn36Cra0txowZ0+Rri4qKoKOjI1Z/urq6KCwsFKsNQnLIiNVKhw8fxrNnzxAVFdXsa7lcLjQ0NMTqT01NDeXl0veICtEwMmK1ApfLxS+//AIPDw+R9urjcrlifb4C3t/L4nJlc8NQeUSC1Qq///47qqqqsHbtWpFez+Vyoa6uLlafJFiyhQSrhYqLi7Fnzx6sXLkSurq6Ir2HihFLTU0NfD4flZWVYrVDSAYJVgv99ttvUFBQwNKlS0V+T3l5OSUjFgAyaskIEqwWKCgowP79+/HDDz+0aDKCqkvBurYI6UeC1QLbtm1Du3btsGTJkha9j6pLQQBkZlBGkOl2EeXm5sLHxwdbtmxp8ehTWVkJFRUVsfonwZItZMQS0fbt26GlpQV3d/cWv7e2trbRdYSiqjs0jsfjidUOIRkkWCJ49+4dfH19sWrVKrRr1/JH7Pl8PhQUFMSqoe79/P+dzkhINxIsEezatQvKysqNPhbSHB6PJ/YxpWTEki0kWM0oKSnBwYMHsWLFilbP7FEZLDJiyQYSrGb88ccfEAqFLZ4J/BCVl4JkxJINJFhNKC8vx759++Dp6SnWwdjkUlD+kGA1wdfXFxUVFVi2bJlY7ZBLQflDgtUIPp+Pffv2wcXFBR06dBCrLYFAIPbBb+RSULaQYDXi9OnTePXqldijFQCw2WwIBAKx2qgbqcT9rEZIBglWI3bv3g1ra2v06tVL7LYUFRXFHmnq3i/uJSUhGeT/UgNu3LiBxMRE7N69m5L2OBwOamtrxWqj7v3iruAgJIOMWA34/fffMXLkSIwePZqS9siIJX/I/6X/eP78Oc6dO4fQ0FDK2lRUVBR7xCLBki1kxPqPQ4cOQV9fH9bW1pS1yeFwyIglZ0iwPlBdXY0TJ05g0aJFlP4F/vBSUCgUorCwEC9evEBxcXGj78nIyEBeXh6q/3f0T937yWcs2UDOx/qAv78/XF1d8erVK+jr67eqjcDAQDx48ABFRUUoLCxEQUEB7ty5A2VlZfD5/I+eAE5LS2v0wLjBgwcjJSUFAKCsrAx1dXUUFRVhwIABMDQ0hI6ODrS1taGtrQ1vb2+xn/ciKMbsKULSZdSoUUJbW1ux2ti7d68QgFBBQUEIoNF/unTp0mQ7q1atarINBQUFIZvNFo4ePVqsegl6kEvB/3nw4AESEhLEWmwLAM7OzlBVVW1y6RGHw4GVlVWT7UyZMqXJNvh8PoRCIby8vFpdK0EfEqz/OXToEPr374+JEyeK1Y6GhgacnZ2b/CzE4/Fgbm7eZDvjx49v9qHKDh06YNasWa2qk6AXCRaAqqoqhISEwNXVtcnDDUS1bNmyJmcB2Ww2TE1Nm2xDWVkZEyZMaHSNIYfDwdKlS8lkhpQiwcL7U0O4XC7s7e0paa9Pnz6YNGlSgzOLLBYLw4cPF+kxlGnTpjUaLKFQCDc3N7FrJehBggXgxIkTMDc3R+fOnSlrc/ny5Q2OWhwOB9OnTxepDXNz80bbsLOzQ6dOncSuk6AJ07MnTMvNzRUqKioKw8LCKG2Xz+cLDQwMGpzRu3PnjsjtfPbZZw22kZiYSGm9BLXkfsQKCAiAmpqayKOIqNhsNpYtW/bJ5aCmpiaGDx8ucjuWlpYffY5is9kYNmwYRo4cSVmtBPXkPlj+/v6YN28eLTdYXV1dPwqWoqIiLCwsWvTQY0OXg99//z1lNRL0kOtgPX78GCkpKVi4cCEt7WtpacHBwaF+xBEKhZg6dWqL2jAzM/soiO3bt8ecOXMorZOgnlwH68yZM+jUqVOzR52K48Opdz6fDzMzsxa9v+7SkcVigcPhwNPTE8rKynSUSlBIroMVHh4OGxsbsfejaMqAAQMwduxYAICRkRG6du3a4jamT58OoVAIgUDQqi2uCcmT22BlZmYiOTkZNjY2tPe1fPlyAGj1BEndKg0bG5tWLw4mJEtug3X69GloaWmJvYRJFDNmzEC3bt2aXcbUmBEjRkBXV7dFh90RzJLbp+bCw8NhbW1N65IgHo+HJ0+eIDMzExMnTqz/99YwNTVFfn4+EhMTYWxs3KKD7wjJk8vnsfLy8qCvr4/w8HDMnDmT0rZra2sRERGB4KAgXL16FVwazrNis9n4YvgI2MyZDScnJ3Ts2JHyPgjxyGWwjh8/jsWLF6OgoEDsI0w/FBYWhlUrV+L16zeYMvwLzDAZg1H9BsCoqwFUlMSfyePx+cjOf4uk509x8V4iTt+MQ0V1FbyWL8eaNWvIKCZF5DJYdnZ2KC4uxqVLlyhpLy0tDUsWL8aN+Hg4fTUV6xd8jW569K/jq6yphu/F89gQdALKKir4ffcuzJ8/n/Z+iebJ3eQFn8/H1atXW3yjtjHR0dEYZWKCyrcFSNjlg6PLvCUSKgBQUVKG54zZeHL4JGaOMIG9vf3/sXfmcVFV/R//zMYOsrogiiugZqmYWGoqqYCiqSUoiGSlaCYJPaL2aLmmabmULZqamuWW8bjlkkupKaQ+igug+dMCNYZhE4Z1lvv7w4ZHAoaZufvMeb9evKqB+z2fe7qfOd9z7lmQnJxM9ncXADbXYp05cwYDBw7ErVu3EBAQQCvW+vXrMWvWLMSFDsOGme/ATs7v2qjdZ07h1dUrMGToEOzavZv2geIEy7G5FuvIkSPo0KEDbVNt27YNiYmJWBL3Gr5Omsu7qQAg+oVQnF6xFum/nseE8eNJy8UjNmksujPZz507h4SpUzEvKhbzotiZZ2gpfYO64sdFH+LkiROYM2cO33JsFpsy1sOHD3Ht2jVa/SuVSoUxo0cjss9zWDLpdQbVMUfvzoHYlJiC1atXY8+ePXzLsUlsyliHDx+Gg4MDrdkW8+fPh71Uhq1J8yCVCLf6Jgx6EW+ERSI5KanOXoYEbhDuk8ECx44dQ2hoqMVrr65cuYLNmzdj1eRpcBHBBpkrJk9FVXk5li9fzrcUm8NmjKXT6XD69GkMHTrU4hgL5s9HSFBXjB9ofIcloeDp6oZ/R03EmtWrUVhYyLccm8JmjHX58mUUFRVZbKycnBwcOXoU74yOYmSLNK6YEj4SdnI5tm/fzrcUm8JmjPXTTz/B19cXXbp0sej6/fv3w9XJCSND2FsUyQYujo4Y3bc/Un/4gW8pNoVNGWvo0KEWtzanT51C6NO9oBDhMTphvfrgQloaqqqq+JZiM9iEsSoqKpCWlmb2svgnuZaRgZ4dOzGoijt6duwMrVaLrKwsvqXYDDZhrF9++QU1NTV48cUXLY6Rp1TCz1ucyzP8vH0AAHl5eTwrsR1swlg//fQTnnrqKbRq1criGBWVlXBu4pACoWLQTd5ncYfNGIvOMDvweOsysWLoV4r5HsSG1RtLqVTi5s2btNJAAsFcrN5YZ86cgVQqrd2CjEDgApswVs+ePdGsWTO+pRBsCJswFhdbnJnLuZvXsWzXN/U+z1Ep8eZnq3lQRGASqzZWUVERbty4gRdeeIFvKfXo36078h8VY+nO/001ylEpEbtyCZLGRPGojMAEVm2ss2fPgqIowfav1iUkQlVagqU7t9eaakvSXHT2NX8baoKwsGpjnTlzBt27d4eXlxffUhplXUIi/sjPw8CURGIqK8LqjSXE/tWT5KiUyM7NwcDuPbD37M98yyEwhNUaq6ysDFevXhVk/8pAjkqJmA+XYPOsFGxNngdlSRE+2L2Db1kEBrBaY507dw46nQ4DBgzgW0qDPGmqQL+2AB6nhcRc1oHVGuvMmTMIDAwU7Mnyej1Vx1QG1iUkomfHzjypIjCF+BYXmYjQ+1ftWrRs9HcRvUM4VEJgA6tssSoqKnDp0iVB968I1o1VGuvChQuoqakRbP+KYP1YpbHOnDmDDh06oE2bNozFlMvl0On0jMXjEu3fW03LRbitgFixWmMx3b9q5uaGknJxLhQ06HZ3d+dZie1gdcaqqanBb7/9xnj/qmOHjrj9IJfRmFxx634OAKBTJ3Hu2SFGrM5Y6enpqKioYNxYvXoH48KtTEZjckVadia8PD0ZTY0JxrE6Y509exZ+fn7o0KEDo3HDw8Nx8VYWHhYWMBqXC/an/4rwiAhRbTQqdqzOWOfPn2dlNntYWBg8PTyw+fhhxmOzSXZuDs7duIbY2Fi+pdgUVmesixcvIiSE+ResDg4OSJg2DWv370NhaSnj8dnivR1bENCpM8LCwviWYlNYlbHu3buH/Px8VowFAHPmzIGDsxPe27GFlfhMc+7mdXx/7mesXrsGUqlV/a8WPFZV2+np6VAoFOjZsycr8V1dXbF8xQps+PEA0gU+kFFRXYXpn6/B8IgIDB8+nG85NofVGevpp5+2+PwrU4iLi0NY2DCMWboAuap81sqhg57SY9LHHyCvtASfrl/PtxybxOqMxVYaaEAikWDnrl3watkCIxe/i9KKclbLs4R3t36Fg+nn8UNqKtq3b8+3HJvEaoyl0Whw9epV1o0FAG5ubjh46BDyy8swICUROSol62Waglanw4zP12LVvl34atMmMleSR6zGWBkZGaisrESfPn04Ka9du3a4kJYGyskBIcnTcSHrJiflNkaxugyRi+Zh68mj2Lt3LyZNmsSrHlvHaoyVnp6OZs2aISAggLMy/f398ev58wgOCcGA2TOR8OnHKCh9xFn5AKDT67HhxwMImDoRNx7m4szZsxg7diynGgj1sRpj/fbbb+jTpw/nw8qurq44eOgQtm3fhsMZlxAwZSLe37GF9fSworoK208eQ/DbUzHzy08w6bXXcDMzE8HBwayWSzANCWUlR1AEBQVh3LhxWLJkCW8a1Go1Vq1ahQ1ffol8lQo9OnVGSOcuCPRrAyd7+kcAaXRa3C9Q4crdOzhz/Sp0ej1Gjx6NRYsXIygoiIE7IDCFVRirpKQEXl5e2L9/PyIjI/mWg5qaGpw6dQpHjhzBfy9fxr2791BYVIiq6mqLY8rlcjRzc0NrX190f+YZhIaGYuTIkfDx8WFQOYExKCvg2LFjFAAqLy+PbymN4uTkRH399dcWXVtWVkYBoA4fPsysKAJrWEUfKz09He3btxfsjkzl5eWoqKiwuHVxcXGBo6MjVCoVw8oIbGE1xuLi/ZWlGAxBJ23z9vZGQYH4lqzYKlZhLLZmtDOFwVjNm1t+OLiPjw9psUSE6I119+5d5Ofnc/Zi2BKYaLGIscSF6I3F9ox2JlCpVHBycoKzs7PFMYixxIXojXXp0iV0796d1RntdMnPz6c9LE6MJS5Eb6z//ve/gp9tUFxcDE9PT1oxvLy8UFRUxJAiAtuI2lgURSEjIwPPPPMM31KMolar4erqSiuGs7MzysuFt0SF0DCiNlZOTg6Ki4vRo0cPvqUYRa1W0+pfAY/fZanV4tww1BYRtbGuXr0KiUSC7t278y3FKGq1Gi4uLrRiEGOJC9Ebq1OnTnBzc+NbilGYaLGcnZ2h0+lQWVnJkCoCm4jaWBkZGYJPA4HHU5qYaLEAkFZLJIjaWFevXhX8wAXAXCpoiEUQPqI1VklJCf744w/RGIuJVBAAGRkUCaI1VkZGBiiKEkUqWFlZSfsFNjGWuBCtsa5evQovLy/4+fnxLaVJNBoNFAoFrRiGQ+O0Wi0TkggsI1pjZWRkCHp+4JPodDrIZDJaMQzX6/4+nZEgbERrrKtXr4oiDQQetzJ0jyklLZa4EKWxNBoNMjMzRTFwATBrLNJiiQNRGiszMxPV1dWiabGYTAVJiyUORGmsq1evwt7eHoGBgXxLMQmSCtoeojRWRkYGnnrqKdojbVxBUkHbQ7TGEksaCAB6vZ72Dr0kFRQXojTWtWvXRDNwAQBSqRR6vZ5WDENLRbevRuAG0Rnr/v37KCgoEJWx5HI57ZbGcD3dlJLADaIz1o0bNwAA3bp141mJ6SgUCmg0GloxDNeLpV9p64jOWJmZmWjZsiW8vLz4lmIypMWyPURnrKysLHTt2pVvGWYhl8tpt1jEWOJCdMbKzMwUnbEUCgVpsWwM0RkrKysLXbp04VuGWTyZClIUhaKiIty9exclJSWNXnPr1i0olUpU/330j+F60scSB6I6H+vhw4do3bo1Tp8+jUGDBvEtp0G+/fZbZGRkoLi4GEVFRSgoKMBvv/0Ge3t76HS6OiuAs7KyGj0w7umnn8b169cBAPb29nBxcUFxcTG6desGf39/eHp6wsPDAx4eHkhJSRH0hqU2Ca+HCJnJTz/9RAGglEol31IaZd26dRQASiaTUQAa/WnVqpXROLNnzzYaQyaTUVKplHruuec4ujOCOYgqFczMzISXlxetUzvYZvLkyXBycjI69UihUGDkyJFG4wwbNsxoDJ1OB4qiMGvWLIu1EthDVMbKysoS/PsrV1dXTJ482WhfSKvVIiwszGicAQMGwMHB+LnF3t7eGDNmjEU6CewiKmOJZUTw7bffNjoKKJVKERoaajSGvb09XnjhhUbnGCoUCiQmJpLBDIEiOmOJYUSwc+fOGDRoUIND4xKJBMHBwXB3d28yzvDhwxs1FkVRmDJlCm2tBHYQjbHy8/NRUFAgihYLAJKSkhpstRQKBSIjI02KERYW1miM6OhowZ65TBCRsTIzMwFANMYaMWIE2rRpU+/zmpoahIeHmxQjKCgIrVu3rve5RqNBYmIibY0E9hCVsdzd3eHr68u3FJOQSqV4++2366WDbm5uZp3nNWLEiDr9KKlUil69egn6aFiCiIwlxjmCr7/+eh1jyeVyhIeHm7XosaF0MDk5mTGNBHYQjbHEMiL4JO7u7oiLi6ttcSiKQkREhFkxhgwZUseIzZo1wyuvvMKoTgLziMZYN2/eFMWI4D95cuhdp9NhyJAhZl1vSB0lEgkUCgVmzpwJe3t7NqQSGEQUxiouLoZSqRRdiwU8XpDZr18/AEBgYKBFW2JHRkaCoijo9XokJCQwLZHAAqIwlmFEUIwtFvB46B2AycPs/8QwS2Ps2LGiGbyxdURhrNu3b8PR0bHB4WsxMGrUKLRt27bJaUyN0bt3b3h5eZEhdhEhilVzd+7cQceOHWlvIcY1Wq0Wt2/fxr179zBw4MDaf7eE0NBQqFQqpKeno2vXrnB1dWVYLYFJRLEeKzo6GhqNBj/88APfUppEo9EgNTUVO7/7DidOnICahfOspFIpng3ujbGvvIxXX31V0LP9bRVRGKtXr14YMmQIVq5cybcUo+zZswez//UvPHjwEMOCn8WokOfRN6gbAv3awNGO/kieVqdDriofV/7vdxy9nI7vfz2DiuoqzEpKwr///W/SigkIURjLzc0NH330EaZOncq3lAbJysrC9GnTcPbcObw6NALvx8SjrQ/78/gqa6qx6eghLPxuG+wdHfHxmtWYMGEC6+USmkbwnZa8vDyUlZWhc+fOfEtpkCNHjqBvSAgq8wuQtvoLbH47hRNTAYCjnT1mjnoZtzfuwEu9QxAbG4vk5GSyv7sAELyx7ty5AwCCNNb69esxcuRIjO3bH2dXfoJnAxrev4JtvNzc8MWMZOyc8x6++OxzjH7pJXJWMc8I3li///47HB0dBff+Ztu2bUhMTMSSuNfwddJc2Mn5X3AY/UIoTq9Yi/Rfz2PC+PGk5eIRwRtLiEPt586dQ8LUqZgXFYt5URP5llOHvkFd8eOiD3HyxAnMmTOHbzk2i3Ce1ka4c+eOoNJAlUqFMaNHI7LPc1gy6XW+5TRI786B2JSYgtWrV2PPnj18y7FJBG+s33//HZ06deJbRi3z58+HvVSGrUnzIJUIt/omDHoRb4RFIjkpqc5ehgRuEO6T8Tf/93//JxhjXblyBZs3b8aqydPgIoINMldMnoqq8nIsX76cbyk2h6CNpVQqUVpaKphUcMH8+QgJ6orxA43vsCQUPF3d8O+oiVizejUKCwv5lmNTCNpYv//+OwBhDLXn5OTgyNGjeGd0FCQSCd9yTGZK+EjYyeXYvn0731JsCkEb686dO4IZat+/fz9cnZwwMuR5vqWYhYujI0b37Y9UEcyztCYEbazff/9dMEPtp0+dQujTvaAQ4TE6Yb364EJaGqqqqviWYjPw/8QaQUhD7dcyMtCzozAGUcylZ8fO0Gq1yMrK4luKzSBoYwlpqD1PqYSftziXZ/h5+wB4PO+SwA2CNpaQhtorKivh3MQhBULFoJu8z+IOwRqrqKgIpaWlaN++Pd9SADzeukysGEYxxXwPYkOwxvrjjz8AAP7+/vwKIRAsQNDGkkgkaNu2Ld9SCASzEbSxWrVq1eThawSCEBGssf7880+rTgPP3byOZbu+qfd5jkqJNz9bzYMiApMI2ljt2rXjWwZr9O/WHfmPirF05/+mGuWolIhduQRJY6J4VEZgAsEa648//rBqYwHAuoREqEpLsHTn9lpTbUmai86+5m9DTRAWgjaWNaeCBtYlJOKP/DwMTEkkprIiBGms4uJiPHr0yOpbLOBx+pedm4OB3Xtg79mf+ZZDYAhBGuvPP/8EYP3vsHJUSsR8uASbZ6Vga/I8KEuK8MHuHXzLIjCAII1lC++wnjRVoN/j+1yXkEjMZSUI1ljNmzeHk5MT31JYQ6+n6pjKwLqERPTsKIwZ/QTLEeTiImsfageAdi1aNvq7iN4hHCohsIFgWyxrNxbBuiHGIhBYQJDGEuJ0JrlcDp1Oz7cMi9D+vdW0XITbCogVwRmrtLQUxcXFgjNWMzc3lJSLc6GgQbe7uzvPSmwHwRnLcJSo0FLBjh064vaDXL5lWMSt+zkAIJjV2LaA4IyVm/v44RXaO6xevYNx4VYm3zIsIi07E16enqI9HF2MCNJYHh4ecHFx4VtKHcLDw3HxVhYeFhbwLcVs9qf/ivCICFFtNCp2BGesBw8eoHXr1nzLqEdYWBg8PTyw+fhhvqWYRXZuDs7duIbY2Fi+pdgUxFgm4uDggIRp07B2/z4UlpbyLcdk3tuxBQGdOiMsLIxvKTaFII3l5yfMpRNz5syBg7MT3tuxhW8pJnHu5nV8f+5nrF67RhC7CdsSgqvt+/fvC7LFAgBXV1csX7ECG348gHSBD2RUVFdh+udrMDwiAsOHD+dbjs0hOGMJNRU0EBcXh7CwYRizdAFyVfl8y2kQPaXHpI8/QF5pCT5dv55vOTaJoIxVVlaG0tJSQRtLIpFg565d8GrZAiMXv4vSCuGdTv/u1q9wMP08fkhNFcyGp7aGoIz14MEDABBsH8uAm5sbDh46hPzyMgxISUSOSsm3JACPpy7N+HwtVu3bha82bcKAAQP4lmSzCNJYQm6xDLRr1w4X0tJAOTkgJHk6LmTd5FVPsboMkYvmYevJo9i7dy8mTZrEqx5bR1DGun//PhwcHODl5cW3FJPw9/fHr+fPIzgkBANmz0TCpx+joPQRpxp0ej02/HgAAVMn4sbDXJw5exZjx47lVAOhPoIy1oMHD+Dr6yuqGQKurq44eOgQtm3fhsMZlxAwZSLe37GF9fSworoK208eQ/DbUzHzy08w6bXXcDMzE8HBwayWSzANCSWgIyhmzJiB69ev48yZM3xLsQi1Wo1Vq1Zhw5dfIl+lQo9OnRHSuQsC/drAyZ7+VtkanRb3C1S4cvcOzly/Cp1ej9GjR2PR4sUICgpi4A4ITCEoY40ePRpOTk747rvv+JZCi5qaGpw6dQpHjhzBfy9fxr2791BYVIiq6mqLY8rlcjRzc0NrX190f+YZhIaGYuTIkfDx8WFQOYEpBGWs3r17Y/DgwVi1ahXfUhjH2dkZn332GV599VWzr1Wr1XB1dcXhw4fJy16RILg+lhhGBM2lvLwcFRUVFrcuLi4ucHR0hEqlYlgZgS0EYyytVguVSmWVxjIYgk7a5u3tjYIC8S1ZsVUEY6yHDx9Cp9MJ/uWwJRiM1by55YeD+/j4kBZLRAjKWADg6+vLsxLmYaLFIsYSF4Ix1l9//QWJRIKWLRvfyFKsqFQqODk5wdnZ2eIYxFjiQjDGysvLg4eHB+zt7fmWwjj5+fm0h8WJscSFYIylVCrRokULvmWwQnFxMTw9PWnF8PLyQlFREUOKCGwjKGNZYxoI/O89FB2cnZ1RXi68JSqEhhGMsfLy8qy2xVKr1bT6V8Djd1lqtTg3DLVFBGMsa2+x6G7nRowlLgRjLNJiGcfZ2Rk6nQ6VlZUMqSKwiWCMlZ+fb7XGKi8vZ6TFAkBaLZEgCGOVlZWhvLycpIJGIMYSF4IwVl5eHgBYbYvFVCoIgIwMigRBGEupfLza1lpbrMrKSjg6OtKKQYwlLgRhrLy8PEgkEqtdtKfRaKBQKGjFMBwap9VqmZBEYBlBGEupVMLLy4v2wydUdDodZDIZrRiG63V/n85IEDaCMZa1poHA41aG7jGlpMUSF4IwljW/wwKYNRZpscSBIIxl7S0Wk6kgabHEgSCMRVqspiGpoLgQhLGsedYFQFJBW0QwxrLWoXYA0Ov1tA9+I6mguODdWJWVlaioqIC3tzffUlhDKpVCr9fTimFoqej21QjcwLuxDFt6WbOx5HI57ZbGcD3dlJLADbwbq7CwEIB1G0uhUECj0dCKYbjeWl+iWxu8G8vQYonwU3rHAAAgAElEQVTl6B5LIC2W7SEIY8lkMri7u/MthTXkcjntFosYS1zwbqzCwkJ4enrSHjUTMgqFgrRYNgbvT3NBQYFV96+AuqkgRVEoKirC3bt3UVJS0ug1t27dglKpRPXfR/8Yrid9LHHA+zE+iYmJuHr1qmgPm/sn3377LTIyMlBcXIyioiIUFBTgt99+g729PXQ6XZ0VwFlZWY0eGPf000/j+vXrAAB7e3u4uLiguLgY3bp1g7+/Pzw9PeHh4QEPDw+kpKTQXu9FYBbe84qCggKrGrgoLCzEqlWrIJPJ6sySqKqqqvN3rVq1MnoKY3h4ODIzM6HT6VBdXV3bcl2/fh3Xr1+HTCYDRVEICQnB+++/z87NECyG91SwsLDQqlLByZMnw8nJyejUI4VCgZEjRxqNM2zYMKMxdDodKIrCrFmzLNZKYA/ejWVtfSxXV1dMnjzZaF9Iq9UiLCzMaJwBAwbAwcH4ucXe3t4YM2aMRToJ7CIIY1lTKggAb7/9ttFRQKlUitDQUKMx7O3t8cILLzQ6WqpQKJCYmEgGMwQK78aytlQQADp37oxBgwY1ODQukUgQHBxs0nu74cOHN2osiqIwZcoU2loJ7MCrsaqqqlBeXm51LRYAJCUlNdhqKRQKREZGmhQjLCys0RjR0dFWvdRG7PBqLGuegDtixAi0adOm3uc1NTUIDw83KUZQUFCDZzJrNBokJibS1khgD2IslpBKpXj77bfrpYNubm4IDg42Oc6IESPq9KOkUil69eqFPn36MKaVwDy8GstwkBrdQ9mEyuuvv17HWHK5HOHh4WZN32ooHUxOTmZMI4EdeDVWcXExJBKJ1U7AdXd3R1xcXG2LQ1EUIiIizIoxZMiQOkZs1qwZXnnlFUZ1EpiHd2O5urpa9arYJ4fedTodhgwZYtb1htRRIpFAoVBg5syZVnlOs7XBq7FKSkrg4eHBpwTW6datG/r16wcACAwMhJ+fn9kxIiMjQVEU9Ho9EhISmJZIYAHejWWtaeCTJCUlAYDJw+z/xDBLY+zYsfD19WVMF4E9iLE4YNSoUWjbtm2T05gao3fv3vDy8iJD7CKC19ntxcXFVp0KarVa3L59G/fu3cPAgQNr/90SQkNDoVKpkJ6ejq5du8LV1ZVhtQQm4XU91ogRI9C8eXN8/fXXfElgHI1Gg9TUVOz87jucOHECahbOs5JKpXg2uDfGvvIyXn31VTRv3pzxMgj04NVY/fr1Q0hICFavXs2XBEbZs2cPZv/rX3jw4CGGBT+LUSHPo29QNwT6tYGjHf2RPK1Oh1xVPq783+84ejkd3/96BhXVVZiVlIR///vfpBUTELwaq2vXroiOjhb9Qr2srCxMnzYNZ8+dw6tDI/B+TDza+rA/j6+yphqbjh7Cwu+2wd7RER+vWY0JEyawXi6haXgfvBB7H+vIkSPoGxKCyvwCpK3+ApvfTuHEVADgaGePmaNexu2NO/BS7xDExsYiOTmZ7O8uAHg3lphHBdevX4+RI0dibN/+OLvyEzwb0PhSezbxcnPDFzOSsXPOe/jis88x+qWXyFnFPMObsaqrq1FZWSlaY23btg2JiYlYEvcavk6aCzs5/wsOo18IxekVa5H+63lMGD+etFw8wpuxiouLAUCUqeC5c+eQMHUq5kXFYl7URL7l1KFvUFf8uOhDnDxxAnPmzOFbjs3Cm7EMe+qJrcVSqVQYM3o0Ivs8hyWTXudbToP07hyITYkpWL16Nfbs2cO3HJuEd2OJrcWaP38+7KUybE2aB6mE950NGmXCoBfxRlgkkpOS6uxlSOAG3lNBMbVYV65cwebNm7Fq8jS4iGCDzBWTp6KqvBzLly/nW4rNwZuxSktLIZPJ4OzszJcEs1kwfz5Cgrpi/EDjOywJBU9XN/w7aiLWrF5de1wSgRt4NZarqyskEglfEswiJycHR44exTujo0SjGQCmhI+EnVyO7du38y3FpuDNWGVlZaKagrN//364OjlhZMjzfEsxCxdHR4zu2x+pP/zAtxSbghjLRE6fOoXQp3tBIcJjdMJ69cGFtLR6+8cT2INXY7m5ufFVvNlcy8hAz46d+JZhET07doZWq0VWVhbfUmwG3vtYYiFPqYSftziXZ/h5+wAA8vLyeFZiO5BU0EQqKivh3MQhBULFoJu8z+IOkgqaCM/n89HCMIop5nsQGyQVJBBYgKSCBAILEGMRCCxAjMUT525ex7Jd39T7PEelxJufWcceILYMr30sMQ1eME3/bt2R/6gYS3f+b6pRjkqJ2JVLkDQmikdlBCbgxVharRZVVVU23WIBwLqERKhKS7B05/ZaU21JmovOvuZvQ00QFrwYq7S0FABs3ljAY3P9kZ+HgSmJxFRWBC/GKisrAwCbTgUN5KiUyM7NwcDuPbD37M98yyEwBC/GMswAcHFx4aN4wZCjUiLmwyXYPCsFW5PnQVlShA927+BbFoEBeDGWYWsuMS1yZJonTRXo1xbA47SQmMs64NVYTk5OfBQvCPR6qo6pDKxLSETPjp15UkVgCl4WF1VUVACw7RarXYuWjf4uoncIh0oIbMBbiyWVSuEg0tniBEJT8GKsiooKODo6imrvCALBHHhrscSWBsrlcuh0er5lWIT2762m5SLcVkCs8GYssQ1cNHNzQ0m5OBcKGnSLaQ9HscNbKii2Fqtjh464/SCXbxkWcet+DgCgUydx7tkhRngzltharF69g3HhVibfMiwiLTsTXp6eaNOmDd9SbAbSxzKR8PBwXLyVhYeFBXxLMZv96b8iPCKCDBZxCOljmUhYWBg8PTyw+fhhvqWYRXZuDs7duIbY2Fi+pdgUpI9lIg4ODkiYNg1r9+9D4d+z88XAezu2IKBTZ4SFhfEtxaYgLZYZzJkzBw7OTnhvxxa+pZjEuZvX8f25n7F67RpIpcI9csgaIYMXZuDq6orlK1Zgw48HkC7wgYyK6ipM/3wNhkdEYPjw4XzLsTmIscwkLi4OYWHDMGbpAuSq8vmW0yB6So9JH3+AvNISfLp+Pd9ybBJejFVZWQlHERzc1hASiQQ7d+2CV8sWGLn4XZRWCO90+ne3foWD6efxQ2oq2rdvz7ccm4QXY1VXV8Pe3p6PohnBzc0NBw8dQn55GQakJCJHpeRbEoDHU5dmfL4Wq/btwlebNmHAgAF8S7JZeDFWVVWV6Ge2t2vXDhfS0kA5OSAkeTouZN3kVU+xugyRi+Zh68mj2Lt3LyZNmsSrHluHGIsG/v7++PX8eQSHhGDA7JlI+PRjFJQ+4lSDTq/Hhh8PIGDqRNx4mIszZ89i7NixnGog1IcYiyaurq44eOgQtm3fhsMZlxAwZSLe37GF9fSworoK208eQ/DbUzHzy08w6bXXcDMzE8HBwayWSzANCcXDERT29vbYvHkzJk6cyHXRrKJWq7Fq1Sps+PJL5KtU6NGpM0I6d0GgXxs42dP/ItHotLhfoMKVu3dw5vpV6PR6jB49GosWL0ZQUBADd0BgCs6NpdfrIZPJsHfvXrzyyitcFs0KGRkZuHv3LsaMGVP7WU1NDU6dOoUjR47gv5cv497deyguKUZFZaXF5cjlcjg7OcG/bVt0f+YZhIaGYuTIkfDx8WHiNghMQ3FMeXk5BYA6cOAA10UzTllZGRUYGEgNHDiQ0uv1Rv/22LFjFACqpKTEorK2bdtG2dvbN1kOQRhwvqS0uroaAKyij/Xmm2+iqKgIJ0+ebHLmeGlpKSQSicV7Kfr5+aG6uhoFBQWklRIBnA9eGE5uF7uxtmzZgh07dmDTpk1o3bp1k39fWloKZ2dnyGQyi8rz83u89fT9+/ctup7ALcRYFpCZmYmZM2ciJSUFo0aNMukauqerEGOJC2IsM6mqqkJMTAyeeuopLF682OTr6BrLyckJnp6eyM0V5/YAtgbnfSyxG+utt97Cn3/+iStXrsDOzs7k65g4aK9NmzZ48OABrRgEbuBt8EKMcwX37NmDLVu2YN++fWjXrp1Z1zJx0J6fnx9JBUUC56lgTU0NAJj1bS8E7ty5gylTpiAxMbHOOytTYWKfD19fXzx8+JBWDAI3cG4srVYLQFybR1ZXVyM6OhqdO3fGhx9+aFEMjUZD+8vEy8sLBQXi28zGFuH86TYYS6FQcF20xbzzzju4ffs2Ll++bHEKq9FoaKe/Xl5eKCwspBWDwA2cG0uj0TwuWCQt1r59+/DZZ5/h22+/RUBAgMVxtFot7VTQ29ubGEskkFTQCDk5OZg6dSqmTZuGmJgYWrE0Gg3tVtrLywsVFRWopDHnkMANvBlL6KmgRqPB+PHj4evri9WrV9OOp9VqaX+ZeHl5AQCKiopo6yGwCy+poFQqFfx2XHPnzsW1a9dw8eJFRvbn0Gq1jLRYAFBYWGjSNCoCf/AyeCH0NPDHH3/EmjVrsHXrVnTp0oWRmBqNhrEWi/SzhA8vqaCQjXX//n3Ex8cjPj6e0X0jDOvQ6ODh4QGJRILi4mKGVBHYgnNjMdGJZwutVosJEybA09MTn3zyCaOxpVIpdH8fAGcpMpkM9vb2tYejE4QLSQWf4P3338elS5eQlpZGe17fP3l8IiQ9YwGPD0QnxhI+xFh/c/r0aXz44Yf44osv8MwzzzAeXy6X146I0oEYSxyQVBCAUqlETEwMXn75ZUyZMoWVMmQyGSPGcnJyIsYSATY/eKHX6zFx4kQ4Ozvjq6++Yq0ckgraFjafCi5ZsgRnz57F+fPnaS/rMAZJBW0LXl4QCyUVPHPmDJYsWYK1a9eiV69erJbFVCpIjCUOOE8FdTqdIFoslUqFmJgYREREYMaMGayXp1AoGDNWRUUFA4oIbMLL4AXfxqIoCq+//jqkUim2bdvGyaHXjo6OjBhCLpfXrhAgCBde+lh8p4IrV67EkSNH8Msvv8DT05OTMl1cXHDv3j3acWQyGSODIAR2sbkW67fffsOCBQuwfPlyPP/885yV6+LiwkjfiBhLHNjUcHtxcTGio6MxZMgQvPPOO5yW7ezsDLVaTTsOU8P2BHbhxVh8pIIUReG1116DVqvF9u3bOelXPQmTLRYTgyAEduFluJ2PFmvdunU4ePAgjh8/Dm9vb87Ld3FxYaTFIqmgOGD1Cb979269JQ5KpRLl5eW4fPlync99fHzQtm1bVnRcvnwZc+fOxaJFixAaGspKGU3xZCpIURTy8/ORn5+PkpKSRs8Krqqqglwur/NFRFJBccDq+Vjff/89xo0bZ9LfbtmyBZMnT6ZVXnh4OBYuXIi+ffvWfqZWqxEcHIzWrVvjp59+or0myhRKS0tx/Phx/PXXX8jPz8eDBw9w/fp1XL16Fc2aNUNJSUmtOSIjI3Hw4MEG4xw+fBiRkZEAHrd4BlNptVp07NgRrq6usLOzg6enJ4YMGYI333yT9XsjmAibZwRVVFRQjo6OFACjPwqFwuJzowxkZ2fXxlq3bl3tOVIxMTFU8+bNqYcPHzJxSyZRXV1N+fr6UlKplLK3t6fkcnmD9y2TyaiPPvqo0TiVlZUm1R8A6tChQ5zdH6FpWD94buLEiZRCoWj0gZDL5dTo0aNpl7NixYraciQSCTVq1Cjqo48+oqRSKXX8+HEG7sQ8Vq1a1aihnvy5ePGi0ThjxoxpMk6LFi0orVbL0Z0RTIF1Y/34449GHwqJRELt27ePdjnBwcGURCKpY1ipVErFx8fTvwkLKC0tpVxcXIzeu5OTU5OG2L59OyWVSo229u+99x5Hd0UwFdaNpdFoKA8PD6MPV0VFBa0y8vLy6pjK8COVSimpVEq9//77lE6nY+iOTGfOnDmNttZSqZQKCwtrMkZhYSElk8mMfjHdu3eP/ZshmAXr77Hkcjmio6Mb3LdcoVAgOjqa9vZiqampDW6nptfrodfrsWTJEgwZMgRKpZJWOeaSlJTU6O9kMplJI5Senp547rnnGrw/QwxzTz4hsA8nL4gnTJhQe8rIk2g0GsTGxtKOn5qaavT3er0ep0+fRp8+fTg9VKBFixaYOHFigy/ENRoNBg0aZFKcl19+udEvjoSEBLoyCWzARbOo0+moFi1a1EtjvL29aXe6S0tLjQ6O4O/+Vvv27akLFy4wdEemk52d3WCa6ujoSNXU1JgU4969ew3el7u7O1VVVcXyHRAsgZMWSyqVIjY2tk46qFAoEBcXR/u90o8//tjoFB+ZTAaJRILJkyfj+vXrdd5vcUVgYCAiIiLqvOSVSCTo16+fyVO72rVrh6CgoDqfKRQKvPHGG6I8wM8W4Gyu4D/TQY1GQ/ugAeBxGtiQOeVyOVq2bImTJ09i48aNtE/6oMPcuXPrmF+hUODFF180K8Yrr7xSx4gajYb2C3UCi3DZPLZv3742jfH396cdr6ampt6QtkwmoyQSCTVlyhSqrKyMvmiG6NOnT53RvfPnz5t1/cWLF+uMKPbt25clpQQm4HR2u6Ejr1Ao8Oqrr9KOd+rUqToTWxUKBTw8PHDgwAFs3LgRLi4utMtgirlz59ZOY7K3t0dwcLBZ1wcHB6N58+a1/z19+nRG9REYhksXZ2Vl1X7r3rp1i3a8N998k5LL5bWDA/Hx8bSnRrGFTqerbbEHDx5sUYzp06dTAChnZ2eqvLycYYUEJuG0xQoKCkLXrl3Ro0cPWqcjAo9niH///ffQarXw8PBAamoqtm7dimbNmjGkllmkUinmzp0LAGb3rwy89NJLAIC4uDg4OTkxpo3APJwvdJw0aRLi4+Npx7l06RLy8/MRHR2N27dvY/To0QyoY5dJkyahRYsWGDhwoEXXDx48GG5ubqzt1ktgDsaWjZSVleHmzZv466+/8OjRowZfCAOPz3aSyWRwd3dvNJazszPc3d3Rvn17BAQENLgwctWqVWjVqhUmTpzIhHzWMdTPxo0b8fzzz0Ov11sU59ixYxg7dmyT9UPgGTp5pFKppFasWEH1efZZoxNF6fy4ODtTY0aPpvbs2VPnhapGo6GXBHMAn/VD4BeLWqyysjIsW7YMa9esgZO9A17p9wLCg0PQs2NntPFpDjkDiwkra6px634u0rJv4kD6eRy/fBGtW/ti1UcfISoqinZ8NiH1QzDbWDt37sQ7ycmorqjEwph4vBEeCUc79t/+56iUWPTtNmw9cQQD+vfHF19+ydgxpkxC6ocAmGEsnU6H2bNnY+3atUgYPgpL496AF4uHCDTGxdvZeOvLdch+kItdu3cjIiKCcw0NQeqH8CQmGau8vBzjo6Nx4qcT2Jo8F9Ev8LMhi4EarQYJn36Mb04dx9q1a/HWW2/xqofUD+GfNDmcpNPpMGH8eKT/eh6nV6xF36CuXOgyip1cga+T5iKgtR8SExPh6urKyBC+JZD6ITREk8ZKSUnBT8d/wqnlawTx0DzJvKiJKKuoxJQ3pqBt27YYPHgw5xpI/RAawmgquGfPHowfPx7fzl6ACYMsmy3ANnpKj6jlC/FL1g1kZmXBx8eHs7JJ/RAao1FjqdVqBAUGYvjTwdiY+C+udZmFurISQdPiMGLMGGzYsIGbMkn9EIzQ6JSmDz74ABVlaiyLf4NLPRbh4uiIlZOnYdOmTbh48SInZZL6IRijwRarsLAQbfz8sCzudSSNEcfLRoqi0D9lJjz82+DQ4cOslkXqh9AUDbZY27Ztg51cjqkRo7jWYzESiQTJo8fhyNGjyM3NZbUsUj+EpmjQWP9JTcWY5wbA2cGBaz20GBXSD65OTti/fz+r5ZD6ITRFPWNVVlbiQloahvV8lg89tFDI5Qh9uhdOnzrFWhmkfgimUM9YWVlZ0Gq16NmxMx96aNOzYydcv3aNtfikfgimUM9YeXl5AIA2Ps3r/bEYaO3lgzwWd7wl9UMwhXrGMhzn6STS/epcHB2hZuBI0sYg9UMwhXrGMoy+c31GL5NYsMTM7NikfgjG4HzPCwLBFiDGIhBYgBiLQGAB1o117uZ1LNv1Tb3Pc1RKvPnZaraLFzykfqwT1o3Vv1t35D8qxtKd22s/y1EpEbtyiWjm2bEJqR/rhJNUcF1CIlSlJVi6c3vtQ7MlaS46+/pxUbzgIfVjfXDWx1qXkIg/8vMwMCWRPDQNQOrHuuDMWDkqJbJzczCwew/sPfszV8WKBlI/1gUnxspRKRHz4RJsnpWCrcnzoCwpwge7d3BRtCgg9WN9sG6sJx+aQL+2AB6nPeTheQypH+uEdWPp9VSdh8bAuoRE0c4QZxJSP9YJ68dUtGvRstHfRfQOYbt4wUPqxzohMy8IBBYgxiIQWKCesQyHmGn/PohabOh0elYPYiP1QzCFesYynLT4SKSL4YrVZXBn8RxiUj8EU6hnrA4dOgAAbj8Q5xZZtx/kouPf98AGpH4IplDPWP7+/vDy9MT5rBt86KHNhVuZ6BkczFp8Uj8EU6hnLIlEgvCICBxIP8+HHlo8LCzApdvZrB62RuqHYAoNjgrGxMTg7I0MZOfmcK2HFpuPH4anhweGDRvGajmkfghN0aCxwsPDEdCpM97bsYVrPRZTWFqKtfv3IWHaNDiwvEMtqR9CUzRoLKlUik8/W4+9Z0/j52tXudZkEQu+2QSFgz1SUlJYL4vUD6EpGn1BPHToUIwYPhwzN3yCiuoqLjWZTfqtTGw8cggrV62CG0cHapP6IRjD6ImO9+7dQ59nn8XALk9hz7yFkEqEN1EjV5WPkOTp6NnnWRw6fJjT/f5I/RAaw+iT0L59e/yQmoqD6Rfw7tavuNJkMqUV5Ri5+F14tWyBnbt2cf7QkPohNIZs4cKFC439gb+/P/z9/ZGyfBnyS0oQ1utZSKX8fzPnqJQYOv9fUFWocfLUKbRo0YIXHaR+CA1h0hMwadIk7N27F1tPHkXkonkoVpexrcsoF7JuIiRpOihHB1xIS0O7du141UPqh/BPTP5qHTt2LM6cPYsbD3MRMHUiNvx4ADq9nk1t9SgofYSETz/GgNkzEdw3BL9eOA9/f39ONTQGqR/CkxgdvGiIR48eYfHixfj0k0/R1b8dkkePwyv9B8LJnr13IzkqJTYfO4xPD6bCydUFH65ciZiYGEH2GUj9EAALjGUgOzsb77/3HlJT/wO5TIoXuvdAzw6d0Ma7OeQyGW1h5VWVuP3gPtJ/z8LVO7+juY8PEqZNw+zZs+Hi4kI7PtuQ+rFtLDaWgfz8fBw8eBCnT5/GjWvX8PCvv1Dy6BE0Go3FMZ2dnNDMrRk6dOyAXsHBiIiIQGhoKOzs7OhI5YV/1s/9Bw/wqLQUWq3W4pgO9vbw9PC0ivqxWiiO2b17N8VDsYLh0KFDFABKrVZbdP2WLVsoZ2dnhlURmIb/cWEbQ6VSwcnJCc7OzhZd7+Pjg/LyclRUVDCsjMAkxFgck5+fDx8fH4uvN1xbUFDAlCQCCxBjcYxKpWLEWCqViilJBBYgxuIYlUqF5s2bW3w9MZY4IMbiGLotlqurKxwcHIixBA4xFsfQbbEAwNvbmxhL4BBjcUxRURE8PT1pxfDy8kJRURFDighsQIzFMWq1mvbMCBcXF5SLdF9DW4EYi2OYMJazszPUajVDighsQIzFIXq9HhUVFRa/HDbg4uJCjCVwiLE4pKKiAhRFkRbLBiDG4hCDGZhosUgfS9gQY3GIwVhMDF6QFkvYEGNxCFPGIqmg8CHG4pDKykoAgKOjI604Tk5OZHa7wCHG4hDD4kaFQkErjlwup7VQksA+xFgcYjCDjObSfJlMRowlcIixOMRgBrpHlcrlcuhEelSrrUCMxSEGMzBhLNJiCRtiLA5hMhUkLZawIcbiECZTQdJiCRtiLA4hqaDtQIzFIQZj0T00gaSCwocYi0MMfSs9zT3ddTod7X4agV2IsTjEkALS2SUYeNxXo5tOEtiFGItDDGag2z8ixhI+xFgcYpjKRFos64cYi0NIi2U7EGNxCFMtlkajIcYSOMRYHPLPFqu6uhpKpRK3bt1q9JqSkhLcvXsXRUVFoP4+cUmr1dKeIU9gF/K1xxKVlZVYuXIliouLUVxcjKKiIvz555+QSqXo2bMn1Go1qqurAQDdu3fHtWvXGoyTl5eHLl261P63i4sLZDIZqqurMXDgQHh7e8PT0xMeHh545plnEBsby8n9EYxDjMUSjo6OOHbsGNLT0yGRSOq80C0sLKz9d5lMhvDw8EbjBAUFoVWrVvjrr78AoM7K4TNnztTG0Ol0WLduHdO3QbAQkgqyyNtvvw2KoozOktDpdAgLCzMaJzIy0uhpjTqdDo6OjoiPj7dYK4FZiLFY5OWXX25yn3Z7e3v079/f6N+EhYUZHfCws7PDa6+9hmbNmlmkk8A8xFgsIpfLMWPGjEZH8KRSKQYNGgR7e3ujcYYOHWp0fqFGo8Fbb71FSyuBWYixWGbatGmQSCQN/k4mk2H48OFNxnBzc0OvXr0ajCOXyzF48GAEBQXR1kpgDmIslvHx8UFUVFSDw+MajabJ/pWByMjIBls+nU6HpKQk2joJzEKMxQFJSUkN9pF8fX0RGBhoUozw8PBGY0RERNDWSGAWYiwOCA4ORq9eveos9VAoFBg5cqTJMXr37g13d/c6n8nlciQnJ5MlJAKEGIsjkpKS6qzD0mq1JqeBwOOBjqFDh9ZJB2UyGV599VUmZRIYghiLI6Kiouqc5CiVShEaGmpWjIiIiFpzKhQKxMfH0z4dksAOxFgcYWdnhzfffBNyuRwSiQS9e/c2+73TsGHDaucLajQazJgxgw2pBAYgxuKQadOmgaIoUBSFESNGmH1969atERAQAADo168fnn76aaYlEhiCGItDfH19MXbsWAAwq3/1JJGRkQCA5ORkxnQRmIcYi2MSExPh5eWF3r17W3R9WFgY2rZti1GjRjGsjMAkZHY7R5SVleHmzZtQqVQYMmQINm3aZFEcjUaDQYMG4dixY2jfvj0CAgLIokcBIqEMvWGO2LNnD6Kjo8FxsbyQn5+Pr7/+Gj/s24dLly/T3vasIVycnTF06FBMiInB6OFPQWsAACAASURBVNGjyQJIgUC+6ligrKwMy5Ytw9o1a+Bk74BX+r2AOfMWoWfHzmjj0xxyBl7oVtZU49b9XKRl38SB9POYMH4CWrf2xaqPPkJUVBQDd0GgA2mxGGbnzp14JzkZ1RWVWBgTjzfCI+FoZ3z2OhPkqJRY9O02bD1xBAP698cXX35ZZ+UxgVvI4AVD6HQ6JCcnIzY2Fi8Fh+D2xh2YOeplTkwFAG19WmDzrBSkrf4ClfkF6BsSgiNHjnBSNqE+xFgMUF5ejtEvvYQvPvscO+e8hy9mJMPLzY0XLc8GBOHsyk8wtm9/jBw5EuvXr+dFh61D+lg00el0mDB+PNJ/PY/TK9aib1BXviXBTq7A10lzEdDaD4mJiXB1dSXL9jmGGIsmKSkp+On4Tzi1fI0gTPUk86ImoqyiElPemIK2bdti8ODBfEuyGUgqSIM9e/ZgzZo12DJrDp7r0o1vOQ2yNP51jOr7PKLGjYNKpeJbjs1AjGUharUayUlJeCMsEhMGvci3nEaRSqTYmjQP9lIp5s+fz7ccm4EYy0I++OADVJSpsSz+Db6lNImLoyNWTp6GTZs24eLFi3zLsQmIsSygsLAQa9eswYLxcfBp5t70BQJgwsAX0bdLNyxauJBvKTYBMZYFbNu2DXZyOaZGiGcirEQiQfLocThy9Chyc3P5lmP1EGNZwH9SUzHmuQFwdnDgW4pZjArpB1cnJ+zfv59vKVYPMZaZVFZW4kJaGob1fJZvKWajkMsR+nQvnD51im8pVg8xlplkZWVBq9WiZ8fOfEuxiJ4dO+F6IyebEJiDGMtM8vLyAABtfIzvyS5UWnv5IE+p5FuG1UOMZSbl5eUAAKcm9lsXKi6OjlD/fQ8E9iDGMhPDcpfG9mMXA9a6ZEdIEGMRCCxAjEUgsAAxFoHAAsRYPHPu5nUs2/VNvc9zVEq8+dlqHhQRmIAYi2f6d+uO/EfFWLpze+1nOSolYlcuQdIYsimMWCHGEgDrEhKhKi3B0p3ba021JWkuOvv68S2NYCHEWAJhXUIi/sjPw8CURGIqK4AYSyDkqJTIzs3BwO49sPfsz3zLIdCEGEsA5KiUiPlwCTbPSsHW5HlQlhThg907+JZFoAExFs88aapAv7YAHqeFxFzihhiLZ/R6qo6pDKxLSBTtDHoC2f6Md9q1aNno7yJ6h3CohMAkpMUiEFiAGItAYAFiLDMxHPKm1el4VmIZOp2eHFTHAcRYZuLu/ni7s0ciXSxYrC6De7NmfMuweoixzKRDhw4AgNsPxLmF2O0Huej49z0Q2IMYy0z8/f3h5emJ81k3+JZiERduZaJncDDfMqweYiwzkUgkCI+IwIH083xLMZuHhQW4dDsbERERfEuxeoixLCAmJgZnb2QgOzeHbylmsfn4YXh6eGDYsGF8S7F6iLEsIDw8HAGdOuO9HVv4lmIyhaWlWLt/HxKmTYODyHbwFSPEWBYglUrx6Wfrsffsafx87SrfckxiwTeboHCwR0pKCt9SbAJiLAsZOnQoRgwfjpkbPkFFdRXfcoySfisTG48cwspVq+DG09nItgYxFg0+Xb8eeaUlmPTxB9BTer7lNEiuKh9jli5AWNgwxMXF8S3HZiDGokH79u3xQ2oqDqZfwLtbv+JbTj1KK8oxcvG78GrZAjt37RL1JqNigxiLJgMGDMBXm77Cqn27MOPztYKZ6pSjUmJASiLyy8tw8NAhkgJyDDEWA0yaNAl79+7F1pNHEbloHorVZbzquZB1EyFJ00E5OuBCWhratWvHqx5bhBiLIcaOHYszZ8/ixsNcBEydiA0/HoBOz22/q6D0ERI+/RgDZs9EcN8Q/HrhPPz9/TnVQHgMMRaDBAcH42ZmJia99hpmfvkJgt+eiu0nj7E+apijUuL9HVsQMGUiDmdcwrbt23Dw0CG4urqyWi6hcSQUx0dP7NmzB9HR0VZ/4kV2djbef+89pKb+B3KZFC9074GeHTqhjXdzyGUy2vHLqypx+8F9pP+ehat3fkdzHx8kTJuG2bNnw8XFhYE7INCBGIshMjIycOfOHbz88st1Ps/Pz8fBgwdx+vRp3Lh2DfcfPMCj0lJotVqLy3Kwt4enhyc6dOyAXsHBiIiIQGhoKOzs7OjeBoEpKI7ZvXs3xUOxrFJSUkJ16tSJGjx4MKXX643+7aFDhygAlFqttqisLVu2UM7OzhZdS+AO0seiCUVReO2116BWq/Htt982+a5IpVLByckJzs7OFpXn4+OD8vJyVFRUWHQ9gRuIsWiycuVKHDhwAHv27EGrVq2a/Pv8/Hz4+PhYXJ7h2oKCAotjENiHGIsGP//8M+bPn4+VK1diwIABJl2jUqkYMZZKpbI4BoF9iLEsRKlUIiYmBiNGjMCsWbNMvk6lUqF58+YWl0uMJQ6IsSxAq9UiKioKLi4u2LZtm1lz8Oi2WK6urnBwcCDGEjhkHywLmDNnDi5evIjz58+jmZk7HqlUKnTr1o1W+d7e3sRYAocYy0z279+PNWvW4Ouvv0aPHj3Mvr6oqAienp60NHh5eaGoqIhWDAK7kFTQDH7//XfEx8dj+vTpiI+PtyiGWq2mPTPCxcUF5SLd19BWIMYykcrKSkRHRyMgIACrV1t+6DYTxnJ2doZaraYVg8AuJBU0kenTp+PPP//EpUuXYG9vb1EMvV6PiooKi18OG3BxcSHGEjjEWCbw5Zdf4ptvvsGhQ4fQvn17i+NUVFSAoihGWqzi4mJaMQjsQlLBJrh69SqSk5OxYMEC2htdGloZJlos0scSNsRYRiguLsbYsWPx/PPPY8GCBbTjGYzFxOAFSQWFDTFWI+j1esTGxkKr1WLXrl2QMbCGiiljkcEL4UP6WI2wdOlSnDhxAqdPn4a3tzcjMSsrKwEAjo6OtOI4OTmR2e0ChxirAU6ePInFixfjk08+Qb9+/RiLa1jcqFAoaMWRy+W0FkoS2Iekgv8gNzcXEyZMwLhx4/Dmm28yGttgBrpppUwmI8YSOMRYT6DRaDBhwgR4enpi48aNjMc3mIHuUaVyuRw6gexfSGgYkgo+QVJSEjIyMpCens7KDkcGMzBhLNJiCRtirL/ZtWsXPv/8c+zevRtdu3ZlpQwmU0HSYgkbkgoCuHXrFqZOnYpZs2Zh3LhxrJXDZCpIWixhY/PGUqvVGDt2LLp164YVK1awWhZJBW0Hm08Fp0+fjoKCAhw/fpz1ffkMxpJK6X2fkVRQ+Ni0sdauXYvvvvsOR48eRevWrVkvz9C30uv1tMyl0+kYmQlCYA+bTQXT0tIwZ84cLF26FEOHDuWkTEMKqNFoaMXRarW000kCu9iksfLz8zFu3DgMGzYMc+fO5axcgxno9o+IsYSPzRlLr9cjLi4OMpkMW7du5fSUQ8NUJtJiWT82939nwYIFOHPmDM6dOwcvLy9OyyYtlu1gU/93Dh8+jBUrVmDjxo0IDg7mvHymWiyNRkOMJXBsJhX8888/ER8fj5iYGLz++uu8aPhni1VdXQ2lUolbt241ek1JSQnu3r2LoqKi2qOPtFot7RnyBHZh9Xyshw8fol+/fnW+oSsrK1FUVFRveLtPnz744YcfaJVHURReffVVfPjhh2jZsmXt51VVVejfvz80Gg0uXLgAJycnWuWYQmVlJVauXIni4mIUFxejqKgIf/75J27evAkPDw+o1WpUV1cDALp3745r1641GCc7OxtdunSp/W8XFxfIZDJUV1ejT58+8Pb2hqenJzw8PPDMM88gNjaW9XsjmADb5wT16NGDAtDkz8qVK2mX9dtvv1EAKG9vb+qXX36p/fyNN96g3N3dqTt37tAuwxyee+45SiqVUjKZrNH7lslk1OzZs43GadWqldG6M8Rft24dR3dGaArWjbV69WpKLpcbfTAkEgn1xx9/0C4rJSWFUigUlEwmo6RSKbVq1Srqm2++oSQSCbVv3z4G7sY8du3aRUkkkia/VE6cOGE0zpQpUyg7OzujMRwdHamSkhKO7ozQFKwb6+HDh5RUKm30gZBKpdRzzz3HSFlt2rSpF1smk1GzZs1iJL65aDQaqkWLFkYNYW9vT1VVVRmN8/333xs1qJ2dHTVjxgyO7opgCqwPXrRq1Qr9+/dvdAqPVCrFpEmTaJdz6dIl5Obm1vlMr9eDoiikpqY22odhE7lcjhkzZjQ6gieVSjFo0KAmNwAdOnSo0SlQGo0Gb731Fi2tBGbhZFQwLi6u0RexFEXhlVdeoV3G3r17Gxwp0+v1ePDgAXr37o1NmzbRLsdcpk2b1ui9y2QyDB8+vMkYbm5u6NWrV4Nx5HI5Bg8ejKCgINpaCQzCRbNYVFREKRSKBjvdYWFhjJTh5+dn0iDJ1KlTKY1Gw0iZphIbG9vg/QOgsrOzTYqxaNGiBmNIJBLq4MGDLN8BwVw4abE8PDwQFhZWLyWiKApxcXG041++fBn37983+jdyuRxeXl4YNmwY5y9Xk5KSGnwp7Ovri8DAQJNihIeHNxqD7g69BObh7AVxbGxsvTVECoUCL730Eu3YjaWBwP/WPr300kvIzs7Gyy+/TLs8cwkODkavXr3qLPVQKBQYOXKkyTF69+4Nd3f3Op/J5XIkJyeTJSRChKumsby8nHJ0dKxNYeRyORUVFcVI7LZt2zaYZsnlcqp58+bUf/7zH0bKoYNh2B9PpHA//PCDWTHGjRtX59WFvb09VVhYyJJiAh04a7GcnJwwZsyY2pZFp9MxMkvgv//9L3Jycup8JpPJIJFIMHnyZNy5c4eRVpEuUVFRdU5ylEqlCA0NNStGREQE9Ho9gMctXnx8PO3TIQkswaWLDx06VPtt6+rq2uT7G1OYO3dunZencrmcatu2LXXq1CkGFDPLggULKLlcTkkkEiokJMTs6+/fv1+n1cvIyGBBJYEJODVWTU0N5ebmRgGgXn/9dUZiGtJAuVxOSaVSas6cOVRlZSUjsZnmwYMHtdOPFi9ebFGMwMBACgDVr18/htURmITT2e0KhQIxMTEAUPtPOly5cqU2DezUqRPS0tKwYsUKODg40I7NBr6+vhg7diwAICwszKIYkZGRAIDk5GTGdBFYgGsn//LLL5Svry+l1Wppx3r33XcpOzs7asmSJVRNTQ0D6tjn7NmzlJeXF6XT6Sy6/vjx41Tbtm05fxdHMA/GXuiUlZXh5s2b+Ouvv/Do0SPU1NQ0ZmQMGDAAmzdvbjSWs7Mz3N3d0b59ewQEBDT63ik3NxdXrlxhbedaJjHUj0qlwpAhQyyeBaLRaDBo0CAcO3asyfoh8AgdVyqVSmrFihVUn2efNTrRls6Pi7MzNWb0aGrPnj11WiW9Xm/xtz5X8Fk/BH6xaKFjWVkZli1bhrVr1sDJ3gGv9HsB4cEh6NmxM9r4NIecgReWlTXVuHU/F2nZN3Eg/TyOX76I1q19seqjjxAVFUU7PpuQ+iGYbaydO3fineRkVFdUYmFMPN4Ij4SjnfHZ2UyQo1Ji0bfbsPXEEQzo3x9ffPllnZW1QoHUDwEww1g6nQ6zZ8/G2rVrkTB8FJbGvQEvNze29dXj4u1svPXlOmQ/yMWu3bsFM0+O1A/hSUwyVnl5OcZHR+PETyewNXkuol8wb8YA09RoNUj49GN8c+o41q5dy/taJFI/hH/S5HCSTqfDhPHjkf7reZxesRZ9g/gfgbOTK/B10lwEtPZDYmIiXF1dER8fz4sWUj+EhmjSWCkpKfjp+E84tXyNIB6aJ5kXNRFlFZWY8sYUtG3bFoMHD+ZcA6kfQkMYTQX37NmD8ePH49vZCzBh0Itc6jIZPaVH1PKF+CXrBjKzsuDj48NZ2aR+CI3RqLHUajWCAgMx/OlgbEz8F9e6zEJdWYmgaXEYMWYMNmzYwE2ZpH4IRmh0ruAHH3yAijI1lsW/waUei3BxdMTKydOwadMmXLx4kZMySf0QjNFgi1VYWIg2fn5YFvc6ksaI42UjRVHonzITHv5tcOjwYVbLIvVDaIoGW6xt27bBTi7H1IhRXOuxGIlEguTR43Dk6NF626AxDakfQlM0aKz/pKZizHMD4CzQ5ReNMSqkH1ydnLB//35WyyH1Q2iKesaqrKzEhbQ0DOv5LB96aKGQyxH6dC+cPnWKtTJI/RBMoZ6xsrKyoNVq0bNjZz700KZnx064zuKut6R+CKZQz1h5eXkAgDY+zTkXwwStvXyQp1SyFp/UD8EU6hmrvLwcAODUxH7iQsXF0RHqv++BDUj9EEyhnrEMo+9cHnrNNBYsMTM7NqkfgjFs5qhUAoFLiLEIBBYgxiIQWIB1Y527eR3Ldn1T7/MclRJvfraa7eIFD6kf64R1Y/Xv1h35j4qxdOf22s9yVErErlwimnl2bELqxzrhJBVcl5AIVWkJlu7cXvvQbEmai86+flwUL3hI/VgfnPWx1iUk4o/8PAxMSSQPTQOQ+rEuODNWjkqJ7NwcDOzeA3vP/sxVsaKB1I91wYmxclRKxHy4BJtnpWBr8jwoS4rwwe4dXBQtCkj9WB+sG+vJhybQry2Ax2kPeXgeQ+rHOmHdWHo9VeehMbAuIVG0M8SZhNSPdcL6MRXtWrRs9HcRvUPYLl7wkPqxTsjMCwKBBYixCAQWqGcswyFmWp2OczFMoNPpWT2IjdQPwRTqGcvd3R0A8Eiki+GK1WVwb9aMtfikfgimUM9YHTp0AADcfiDOLbJuP8hFx7/vgQ1I/RBMoZ6x/P394eXpifNZN/jQQ5sLtzLRMziYtfikfgimUM9YEokE4REROJB+ng89tHhYWIBLt7NZPWyN1A/BFBocFYyJicHZGxnIzs3hWg8tNh8/DE8PDwwbNozVckj9EJqiQWOFh4cjoFNnvLdjC9d6LKawtBRr9+9DwrRpcGB5h1pSP4SmaNBYUqkUn362HnvPnsbP165yrckiFnyzCQoHe6SkpLBeFqkfQlM0+oJ46NChGDF8OGZu+AQV1VVcajKb9FuZ2HjkEFauWgU3jg7UJvVDMIbREx3v3buHPs8+i4FdnsKeeQshlQhvokauKh8hydPRs8+zOHT4MKf7/ZH6ITSG0Sehffv2+CE1FQfTL+DdrV9xpclkSivKMXLxu/Bq2QI7d+3i/KEh9UNoDNnChQsXGvsDf39/+Pv7I2X5MuSXlCCs17OQSvn/Zs5RKTF0/r+gqlDj5KlTaNGiBS86SP0QGsKkJ2DSpEnYu3cvtp48ishF81CsLmNbl1EuZN1ESNJ0UI4OuJCWhnbt2vGqh9QP4Z+Y/NU6duxYnDl7Fjce5iJg6kRs+PEAdHo9m9rqUVD6CAmffowBs/+fvTuPqyn//wD+urfbvqLslSUVM8aSXVEositFhSj72HczwxgaQ2aswwihRClKZWfKSJElhAppWiwtlPbt3nt+f/jWT6NU955zz723z/Px8Ph+5fb5vO+ZXp3PPedzPp/FMBvQH9G3Y2BoaCjRGupCjg/xua9evKhNfn4+Nm/ejH1796GbYQesmOiAyeZDoabM3L2R9JwseF+5gH3hIVDT1MB2T084OztL5WcGcnwIQIRgVUlKSsLPGzciJOQceApcDOneE706GUFftyV4CgpiF1ZcVooXb14j9mUiHiW/REs9PcybPx+rV6+GhoaG2O0zjRyfpk3kYFXJzs5GeHg4IiMj8TQ+Hm/fvcPH/HxUVlaK3Ka6mhq0tbTRqXMn9DYzg62tLYYNGwYlJSVxSmXU48ePkZycDHt7+xpf/+/xef3mDfILCsDn80XuS0VZGc2bNZep49PkUBJ2+vRpioVuGfXx40fKyMiIsrKyooRC4Vdfe/78eQoAVVRUJFJfR48epdTV1UX6XkJy2L8uLOMoioKbmxuKiopw8uTJej/X5OTkQE1NDerq6iL1p6enh+LiYpSUlIj0/YRkkGCJydPTE2FhYQgMDESbNm3qfX12djb09PRE7q/qe9+/fy9yGwTzSLDEcOPGDfz000/w9PSEhYVFg74nJyeHlmDl5OSI3AbBPBIsEWVlZcHZ2RljxozBsmXLGvx9OTk5aNmypcj9kmDJBhIsEfD5fDg6OkJDQwM+Pj6Nul8k7hlLU1MTKioqJFhSjqyDJYK1a9fi3r17iImJgXYjVzzKycnBN998I1b/urq6JFhSjgSrkUJDQ7Fr1y4cO3YMPXv2bPT35+bmonnz5mLV0KJFC+Tm5orVBsEsMhRshJcvX8LV1RULFiyAq6urSG0UFRWJPTNCQ0MDxTK6rmFTQYLVQKWlpZgyZQqMjY2xc6fom27TESx1dXUUFRWJ1QbBLDIUbKAFCxYgLS0N9+/fh7KyskhtCIVClJSUiHxzuIqGhgYJlpQjwWqAgwcP4sSJEzh//jw6duwocjslJSWgKIqWM1ZeXp5YbRDMIkPBejx69AgrVqzAhg0bxF7osuosQ8cZi3zGkm4kWF+Rl5cHOzs7DBo0CBs2bBC7vapg0XHxggwFpRsJVh2EQiFcXFzA5/MREBAABRqeoaIrWOTihfQjn7Hq4OHhgevXryMyMhK6urq0tFlaWgoAUFVVFasdNTU1MrtdypFg1eLvv//G5s2bsXfvXgwePJi2dqseblRUVBSrHR6PJ9aDkgTzyFDwPzIyMuDk5AQHBwcsXLiQ1rarwiDusFJBQYEES8qRYH2msrISTk5OaN68OQ4dOkR7+1VhEHerUh6PB4GMbtXaVJCh4GeWL1+Ox48fIzY2FpqamrS3XxUGOoJFzljSjQTrfwICAnDgwAGcPn0a3bp1Y6QPOoeC5Iwl3chQEMDz588xd+5cLFu2DA4ODoz1Q+dQkJyxpFuTD1ZRURHs7OzwzTffYNu2bYz2RYaCTUeTHwouWLAA79+/x9WrVxlfl68qWOJumkCGgtKvSQdr9+7dOHXqFC5fvox27dox3l/VZyuhUChWuAQCAS0zQQjmNNmh4J07d7B27Vp4eHjA2tpaIn1WDQHFWSUY+PRZTdzhJMGsJhms7OxsODg4wMbGBuvWrZNYv1VhEPfzEQmW9GtywRIKhZg+fToUFBRw/Phxie7IUTWViZyx5F+T+6+zYcMG3Lx5E7du3UKLFi0k2jc5YzUdTeq/zoULF7Bt2zYcOnQIZmZmEu+frjNWZWUlCZaUazJDwbS0NLi6usLZ2Rnu7u6s1PDfM1Z5eTmysrLw/PnzOr/n48ePSElJQW5uLqj/7bjE5/PFniFPMEvs/bG+5s2bNzA3N6/xG7qsrAx5eXlfbCDQr18/BAcHi9UfRVGYOXMmtm/fjtatW9fos6qO27dvQ01NTax+GqK0tBSenp7Iy8tDXl4ecnNzkZaWhmfPnqFZs2YoKipCeXk5AKB79+6Ij4+vtZ2kpCR07dq1+u8aGhpQUFBAeXk5+vXrB11dXTRv3hzNmjVDjx494OLiwvh7IxqA6X2CevbsSQGo98/27dvF7uvu3bsUAEpXV5f6559/qr8+e/ZsSkdHh0pOTha7j8YYOHAgxeVyKQUFhTrft4KCArV69eqvttOmTZuvHruq9vfs2SOhd0bUh/Fg/fHHHxSPx/vqDwaHw6FSU1PF7mvNmjWUoqIipaCgQHG5XGrHjh3UiRMnKA6HQ509e5aGd9M4AQEBFIfDqfeXyvXr17/azpw5cyglJaWvtqGqqkp9/PhRQu+MqA/jwXr79i3F5XLr/IHgcrnUwIEDaelLX1//i7YVFBSoZcuW0dJ+Y1VWVlKtWrX6aiCUlZWpsrKyr7Zz5syZrwZUSUmJ+v777yX0roiGYPziRZs2bTB48OA6p/BwuVzMmDFD7H7u37+PjIyMGl8TCoWgKAohISF1foZhEo/Hw/fff1/nFTwulwtLS8t6FwC1trb+6hSoyspKLFq0SKxaCXpJ5Krg9OnT6/w3iqJgZ2cndh9BQUG1XikTCoV48+YN+vTpgyNHjojdT2PNnz+/zpvQCgoKGD16dL1taGlpoXfv3rW2w+PxYGVlBVNTU7FrJWgkidNibm4upaioWOuH7pEjR9LSR/v27Rt0kWTu3LlUZWUlLX02lIuLS63vHwCVlJTUoDZ++eWXWtvgcDhUeHg4w++AaCyJnLGaNWsGGxubL2ZkUxT11bNZQz148ACvX7/+6mt4PB5atGgBGxsbid9cXb58ea03hdu2bQsTE5MGtTFq1Kg62xB3hV6CfhK7QTxt2jQIhcIaX1NUVMSECRPEbruuYSDw/88+TZgwAUlJSbC3txe7v8YyMzND7969a/xiUVRUxLhx4xrcRp8+faCjo1PjazweDytWrCCPkEgjSZ0ai4uLKVVV1eohDI/HoxwcHGhp28DAoNZhFo/Ho1q2bEmdO3eOln7EUXXZH58N4YKDgxvVhoODQ41bF8rKytSHDx8YqpgQh8TOWGpqapg4cWL1mUUgENAySyAuLg7p6ek1vqagoAAOh4NZs2YhOTmZlrOiuBwdHWvs5MjlcjFs2LBGtWFra1t91ldUVISrq6vYu0MSDJFkisPDw6t/22pqatZ7/6Yh1q1bV+PmKY/HowwMDKiIiAgaKqbXhg0bKB6PR3E4HKp///6N/v7Xr1/XOOs9fvyYgSoJOkg0WBUVFZSWlhYFgHJzc6OlzaphII/Ho7hcLrV27VqqtLSUlrbp9ubNm+rpR5s3bxapDRMTEwoANXjwYJqrI+gk0dntioqKcHJyAgA4OzuL3d7Dhw+rh4FGRka4c+cOtm3bBhUVFbHbZkLbtm2r79mNHDlSpDbGjh0LAFixYgVtdREMkHSSb9y4QbVp04bi8/lit/XDDz9QSkpK1JYtW6iKigoaqmNeVFQU1aJFC0ogEIj0/VevXqUMDAwkfi+OaBzabugUFhbi2bNnePfuHfLz81FRUVFXkGFubg5vb+8621JXV4eOjg46duwIY2PjOu87ZWRk4OHDh4ytXEunquOTk5ODESNGiDwLpLKyEpaWlrhy5Uq9x4dgkTipzMrKorZt20b169v3qxNtxfmjoa5OTZo4kQoMDKxxm+BQpQAAIABJREFUVhIKhSL/1pcUNo8PwS6RHnQsLCzEr7/+it27dkFNWQWTBw/BKLP+6NW5C/T1WoJHww3L0opyPH+dgTtJzxAWG4OrD+6hXbu22PH773B0dBS7fSaR40M0Olj+/v5YuWIFyktKscnZFbNHjYWq0tdnZ9MhPScLv5z0wfHrl2Bhbo6/Dh6s8WSttCDHhwAaESyBQIDVq1dj9+7dmDd6PDymz0YLLS2m6/vCvRdJWHRwD5LeZCDg9GmpmSdHjg/xuQYFq7i4GFOnTMH1a9dxfMU6TBnSuBkDdKvgV2Levj9wIuIqdu/ezfqzSOT4EP9V7+UkgUAAp6lTERsdg8htuzHAlP0rcEo8RRxbvg7G7dpjyZIl0NTUhKurKyu1kOND1KbeYK1ZswbXrl5DxG+7pOKH5nPrHaehsKQUc2bPgYGBAaysrCReAzk+RG2+OhQMDAzE1KlTcXL1BjhZDpdkXQ0mpIRw/G0T/kl8ioTEROjp6Umsb3J8iLrUGayioiKYmphg9HdmOLRklaTrapSi0lKYzp+OMZMmwcvLSzJ9kuNDfEWdcwW3bt2KksIi/Oo6W5L1iERDVRWes+bjyJEjuHfvnkT6JMeH+Jpaz1gfPnyAfvv2+HW6O5ZPko2bjRRFwXzNYjQz1Mf5CxcY7YscH6I+tZ6xfHx8oMTjYa7teEnXIzIOh4MVEx1w6fLlL5ZBoxs5PkR9ag3WuZAQTBpoAXUpffyiLuP7D4ammhpCQ0MZ7YccH6I+XwSrtLQUt+/cgU2vvmzUIxZFHg/DvuuNyIgIxvogx4doiC+ClZiYCD6fj16du7BRj9h6dTbCEwZXvSXHh2iIL4KVmZkJANDXaynxYujQroUeMrOyGGufHB+iIb4IVnFxMQBArZ71xKWVhqoqiv73HphAjg/REF8Eq+rquyQ3vaabCI+YNbptcnyIr2kyW6UShCSRYBEEA0iwCIIBjAfr1rMn+DXgxBdfT8/JwsL9O5nuXuqR4yOfGA+W+TfdkZ2fBw9/3+qvpedkwcVzi8zMs2MSOT7ySSJDwT3zliCn4CM8/H2rf2iOLl+HLm3bS6J7qUeOj/yR2GesPfOWIDU7E0PXLCE/NLUgx0e+SCxY6TlZSMpIx9DuPREUdUNS3coMcnzki0SClZ6TBeftW+C9bA2Or1iPrI+52HraTxJdywRyfOQP48H6/IfGpL0BgE/DHvLD8wk5PvKJ8WAJhVSNH5oqe+YtkdkZ4nQix0c+Mb5NRYdWrev8N9s+/ZnuXuqR4yOfyMwLgmAACRZBMOCLYFVtYsYXCCReDB0EAiGjG7GR40M0xBfB0tHRAQDky+jDcHlFhdDR1masfXJ8iIb4IlidOnUCALx4I5tLZL14k4HO/3sPTCDHh2iIL4JlaGiIFs2bIybxKRv1iO328wT0MjNjrH1yfIiG+CJYHA4Ho2xtERYbw0Y9Ynn74T3uv0hidLM1cnyIhqj1qqCzszOinj5GUka6pOsRi/fVC2jerBlsbGwY7YccH6I+tQZr1KhRMDbqgo1+RyVdj8g+FBRgd+hZzJs/HyoMr1BLjg9Rn1qDxeVysW//nwiKisSN+EeSrkkkG04cgaKKMtasWcN4X+T4EPWp8waxtbU1xowejcVee1FSXibJmhot9nkCDl06D88dO6AloQ21yfEhvuarOzr++++/6Ne3L4Z2/RaB6zeBy5G+iRoZOdnov2IBevXri/MXLkh0vT9yfIi6fPUnoWPHjggOCUF47G38cPywpGpqsIKSYozb/ANatG4F/4AAif/QkOND1EVh06ZNm772AkNDQxgaGmLNb78i++NHjOzdF1wu+7+Z03OyYP3TKuSUFOHviAi0atWKlTrI8SFq06CfgBkzZiAoKAjH/76Msb+sR15RIdN1fdXtxGfov3wBKFUV3L5zBx06dGC1HnJ8iP9q8K9WOzs73IyKwtO3GTCeOw1eF8MgEAqZrO0L7wvyMW/fH7BYvRhmA/oj+nYMDA0NJVpDXcjxIT731YsXtcnPz8fmzZuxb+8+dDPsgBUTHTDZfCjUlJm7N5KekwXvKxewLzwEapoa2O7pCWdnZ6n8zECODwGIEKwqSUlJ+HnjRoSEnANPgYsh3XuiVycj6Ou2BE9BQezCistK8eLNa8S+TMSj5JdoqaeHefPnY/Xq1dDQ0BC7faaR49O0iRysKtnZ2QgPD0dkZCSexsfj7bt3+Jifj8rKSpHbVFVVhbamFoy6GKG3mRlsbW0xbNgwKCkpiVMqK/57fDJev0ZhUZFYx0ddTQ3aWtro1LmTzB8feSV2sOhWUVEBPT09/PLLL1i2bBnb5dDO3NwcnTp1gq+vb/0vJmQW+9eF/0NJSQl2dnbw9/dnuxTapaWlISYmBlOnTmW7FIJhUhcsAHBycsLdu3fx4sULtkuh1cmTJ9GiRQtYW1uzXQrBMKkM1vDhw9G6dWsEBASwXQqt/P394ejoCEVFRbZLIRgmlcFSUFCAo6OjXA0H4+Pj8fTpUzg5ObFdCiEBUhks4NNwMCkpCXFxcWyXQgt/f38YGBhg8ODBbJdCSIDUBmvAgAHo0qWLXJy1KIpCQEAAuWnbhEhtsABgypQpOHXqFAQyuoZflVu3biE1NRXOzs5sl0JIiFQHy8XFBW/fvkVUVBTbpYjF398f3bp1Q/fu3dkuhZAQqQ6WqakpevbsiVOnTrFdisj4fD7Onj2LadOmsV0KIUFSHSzg04pIZ86cQXl5OduliOTKlSvIyckhN4WbGKkPlouLC/Lz83H58mW2SxGJv78/Bg0ahI4dO7JdCiFBUh+stm3bwsLCQiavDpaUlCA0NJTcu2qCpD5YwKd7WqGhoSgoKGC7lEYJDQ1FWVkZJk+ezHYphITJRLAcHR0hFAoRGhrKdimN4u/vD2tra7LeRBMkE8Fq1qwZRo4cKVPDwby8PFy5coUMA5somQgW8Gk4eO3aNWRlZbFdSoMEBgaCy+ViwoQJbJdCsEBmgjVhwgSoqKjgzJkzbJfSIKdOncKECRPIyrNNlMwES01NDRMmTJCJ4WBGRgZu3bpFhoFNmMwEC/g0HIyJicG///7Ldilf5e/vD21tbYwaNYrtUgiWyFSwRo4cCT09Pal/ANLf3x8ODg5QVlZmuxSCJTIVLB6PB3t7e/j5+bFdSp2SkpLw6NEjMgxs4mQqWMCn4WBCQgKePHnCdim1OnnyZPVsEaLpkrlgmZubo0OHDlJ7EeP06dNwdnaGAg2LchKyS+aCxeFwMGXKFJw8eRJStiQi7ty5g5cvX5JhICF7wQI+DQfT09MRHR3Ndik1+Pv7w9TUFL1792a7FIJlMhmsHj164Ntvv5Wq4aBAIEBgYCA5WxEAZDRYwKezVmBgoFhroNPp77//RmZmJnmgkQAgw8FycXHBhw8fcO3aNbZLAfBpClO/fv1gbGzMdimEFJDZYBkaGmLgwIFSMRwsKyvDuXPnyDCQqCazwQI+DQdDQkJQXFzMah3h4eEoLCyEg4MDq3UQ0kOmgzVlyhSUl5cjPDyc1Tr8/f0xfPhwtGvXjtU6COkh08HS09PDiBEjWB0OFhQU4NKlS2QYSNQg08ECPg0HL126hA8fPrDSf1BQEABg0qRJrPRPSCeZD9akSZOgqKiIs2fPstK/v78/xowZAx0dHVb6J6STzAdLU1MTY8eOZWU4+O7dO9y4cYMMA4kvyHywgE/DwZs3b+L169cS7TcgIADq6uoYPXq0RPslpJ9cBGv06NHQ0dGR+AOQ/v7+sLe3h6qqqkT7JaSfXATraxuCCwQCsac9lZWVffG1V69e4f79+2QYSNRKLoIFfBoOxsXF4dmzZwCAe/fuYfny5Wjfvj2eP38uVtvbtm1D7969sWvXLrx9+xYA4OfnBz09PVhZWYldOyF/eGwXQBdLS0u0atUKCxcuRHp6OlJTU8Hj8cDn88V+bouiKDx+/Bjx8fFYuXIlzM3N8fLlS9jZ2YHHk5tDSNBI5n8q3rx5gzNnzuDkyZPIysrChw8fwOfzAaD6f8XdnpSiKCgqKlZvJRQTEwOKonDkyBGkpaVh6tSpsLe3h7q6unhvhpAbMjsUTElJwdChQ6Gvr49Vq1bh/v37AP4/TJ+j+0ljgUAAoVAIPp+Pa9euYebMmWjZsiW+//57CIVCWvsiZJPMnrE6duwIPT09cDicWsP0OSYf4a/qu7S0FEOGDAGXK7O/qwgayexPAYfDgY+PD0xMTKCoqMhoX/UFU0FBAWvWrMGUKVMYrYOQHTIbLABQV1fHxYsXoa6uztqZQlFRERYWFvj1119Z6Z+QTjI7FKzSoUMHBAUFYeTIkYz1UdcZi8fjoWXLlggKCmpyy53l5OQgPT0deXl5KC4uRkVFBYBPWy6pq6ujVatWMDQ0bHLHpYrMBwsARowYga1bt2L9+vW1hoCJz1gcDgc8Hg+XLl2Crq4u7e1Lk5ycHPzzzz+4ceMGHjx4gBcvXiA3N7fe71NWVoaRkRG+/fZbDBkyBFZWVujatasEKmafXAQLANasWYMHDx4gJCSk3osZjVVXMI8fP47u3bvT2pe0yM7Ohr+/P/z8/PDgwQNwuVz07NkTvXv3hp2dHTp37gx9fX00a9YMampqUFJSAkVRyM/PR3FxMbKyspCcnIyXL18iISEBa9euRVFREdq1a4cpU6ZgxowZ6NGjB9tvkzFyE6yqixlJSUlISkqqMY2J7jOWgoICVq9eLZcXK+7evYtt27bh/PnzUFFRwbhx47B8+XIMHDgQmpqaX/1eDocDHR0d6OjooF27djXWV+Tz+Xj06BEiIiJw5swZ7Ny5Ez169MDKlSvh5OQkdzfaZfrixX+pqqoiLCyM9osZnwdTUVERQ4YMgYeHB23tS4Po6GhYW1ujf//+SE9Px759+5CQkIA9e/bAxsam3lDVh8fjoU+fPlizZg1iY2Nx4cIFmJqawt3dHcbGxjh06BAEAgFN74Z9chUs4NPFjP/Ochf3jMXhcEBRFHg8Htq0aYMzZ87IzYfy7OxsuLq6wsLCAmVlZTh79iwuX74Me3t7qKioMNInh8NBv3798OeffyI2NhaWlpZYvHgx+vTpg9u3bzPSp6TJXbCAT/tobd26lbb2KIpCRUUFeDwezp8/j+bNm9PWNptOnDgBU1NTRERE4NixYwgJCcGQIUMkWoO+vj48PT1x8+ZNaGtrY/DgwViwYAFKS0slWgfd5DJYwKeLGZMnTwZAz2csDocDX19fubhYUVJSAjc3N7i6usLR0RHR0dEYM2YMqzV17twZZ86cgZeXFwICAtCvXz8kJSWxWpM45DZYVUGgY4MCiqKwdu1auVg3MCMjA/3798e5c+dw4sQJeHh4QE1Nje2yqk2aNAkRERFQUVFB3759ERoaynZJIuFQ0rYXDk0+fvyIJ0+e4PHjx/j48SNatmwpclvJyckYMGAAOnfujK5du0JJSYnGSiUnISEBo0aNgoaGBk6dOoX27duzXVKdKisrsW7dOpw6dQoHDx6Eu7s72yU1ilwF6+3btzh69ChCzgbj4eNH1UNAHU1NcCD6oyMV/EoU/2/Mr6qigmHDhsHZxQX29vYys89wXFwcbGxsYGRkBD8/P5lZVWr79u34448/4OnpiVWrVrFdToPJRbDy8/OxefNm7Nu3D9rq6nA0t8Qos/7obWSMdi3omRXBFwjw/HU67iQlICw2Bhfv3UarVq2w3dMTzs7OYj/zxaTk5GSYm5uja9eu8PX1lbk1Ory8vLBhwwYcPnxYZs5cMh8sX19frFm9GsKKSmyeNguzrEdDmeHZ7gDw9sN7bPb3wZHLFzBgQH8c9PLCt99+y3i/jZWZmYnBgwdDW1sbISEhMvsw5m+//Ya9e/fi7NmzGD9+PNvl1Etmg8Xn87F8+XLs378fi8fbYZPLLDTTEO8mpigevnqJRQf3ID41Baf8/TFu3DiJ11AXPp+P4cOH482bN7h48aLM3yZYvnw5QkNDcf/+fanfLkkmg1VYWAhHBwfc/Ocf+K78AfaDh7JaTyWfj+8P7MbRaxexY8cOLF++nNV6qvz444/YuXMnLl26JJVn08aqrKzE+PHjUV5ejtjYWKke0spcsAQCAcaNHYu4u/dw/uff0KeLCdslVfv9bADWHD0oFZ8FIiMjMWLECPz++++YPn06q7XQKS0tDcOHD8eMGTOwd+9etsupk8wFa+nSpTh00AuR23ZjgGk3tsv5wsYTR7Et6BQuXb6E4cOHs1JDeXk5evTogU6dOsHX15eVGpgUGBiIxYsXIzY2Fn369GG7nFrJVLBOnTqFadOm4fS6TXCwsGS7nFpRFAUnzy24Fh+HxKQkse6ficrDwwNbt25FdHQ09PX1Jd4/0yiKgp2dHUpLSxEbGyuV8zZlJliFhYUwNTHB+N798Nf3K9gu56tKystgOm8GbMaNxZEjRyTad2ZmJjp16oSVK1di6dKlEu1bkp4/fw4rKyscPnwYrq6ubJfzBZmZ0uTh4YHSomJsmS799zHUlFWww20+jh07hrt370q07127dkFTUxPz5s2TaL+SZmJigsmTJ8PDw0MqHzeRiWC9f/8ee/fswSbnmdDV0ma7nAZxtLDCoG7fYtPPP0usz/z8fHh5eWHBggWMPfIhTVasWIHU1FTW9kb7GpkI1vHjx6GipITZo9idgd0YHA4HKyY64srVq0hLS5NIn4cPHwaHw8HMmTMl0h/bOnTogLFjx2Lnzp1sl/IFmQjWuZAQ2A20gJqybP0WHttvILTU1SU2Q9vX1xeTJk2ChoaGRPqTBtOmTUNsbKzUPWIi9cEqLS1F7N27sOndl+1SGk2Rx8Ow73rhRmQk433FxcXhyZMncvFoS2NYWFigXbt2OHnyJNul1CD1wUpISACfz0fPTkZslyKSXp264El8POP9BAYGomPHjlJ7X4cpXC4XkyZNQmBgINul1CD1wcrMzAQAtNeV/P0gOrRtoYus7GzG+4mIiMCwYcOkepY9U6ysrPDixQukp6ezXUo1qQ9WSUkJAEBNRp57+i8NVVUUFRcz2kd+fj7i4uJgbm7OaD/Sqn///lBRUcGNGzfYLqWa1Aer6v61LP8mZvoefHR0NIRCIQYNGsRoP9JKWVkZZmZmuHnzJtulVJP6YBH1S0hIQJs2bRh9LOT169dYunTpV1cZzsrKQlBQEHbv3o3U1FSRXyOKbt26ISEhgbb2xEWCJQeeP38OIyPmLu4IhUIsWrQIp06dqnNjPV9fX8yaNQudOnXC0qVL0aFDB5FeIyojIyOx95qmk3yt69tEMR2sv/76Cx8+fKj13yiKgqurK4qKihASElLrGiANeY24jIyMkJubi5ycHOjp6dHefmM1uTPWrWdP8GvAiS++np6ThYX7pe8OfkNkZWWhdevWjLSdkJCA+Ph42Nvb1/rv+/fvx/3793Hw4ME6A9OQ14irVatWAD6t7CsNmlywzL/pjuz8PHj4//9zSuk5WXDx3ILlkxxZrEx0hYWFjKxlUVFRgU2bNuG3336r9d/j4+OxdetWLFy4sM7HYxryGjpUzTYpLCxkrI/GaHLBAoA985Ygp+AjPPx9q0N1dPk6dGkrvevsfU1RUREj05g8PDywcOHCOi+KHDx4EBRFwdDQEIsXL8aECROwceNGFBQUNOo1dKh6/3S3K6omGSzgU7hSszMxdM0SmQ4V8GktCLr3Ya66dG1paVnna+Li4qCrqwuhUIht27Zh4cKFOHbsGMaPH1999bAhr6FD1RCzvLyctjbF0WSDlZ6ThaSMdAzt3hNBUTfYLkcs6urqKKbxJvTHjx9x4MAB/PTTT3W+Jj8/HykpKbCwsMCECROgrq6OkSNHws3NDc+ePUNwcHCDXkOXqvcv7nZDdGmSwUrPyYLz9i3wXrYGx1esR9bHXGw97cd2WSLT0NCgNVgeHh7gcDjYsmULNmzYgA0bNuDatWsAgE2bNsHf3x/5+fmgKOqLYWL//v0BAE+fPm3Qa+hS9dlKS0uLtjbF0eSC9XmoTNobAPg0LJTlcGlpaSE/P5+29po1a4aKigokJCRU/6m62paYmIj09HTo6+tDQ0Ojei5nlb59Pz2FoKam1qDX0KXqs5W0nLGa3H0soZCqEaoqe+YtwaX7sSxVJZ6OHTvS+jDljz/++MXXdu/ejV9//RWnT5+u3hRi4MCBePLkSY3XvXnzpvrfOBxOva+hS2pqKhQUFKRm8Zwmd8bq0Kr1F6GqYtunv4SroYeJiQlevXol8X63bduG7OxsnDlzpvpr165dg6WlJYYOHdrg19AhOTkZhoaGUrMkQZM7Y8kjU1NT7N+/H0KhkNa9l+tjYGAALy8v/PLLL3j37h0yMzORm5tbYy3DhryGDq9evYKpqSmtbYqDBEsO9O7dGyUlJUhMTMQ333zDSB/Lli3DsmXLvvi6jY0NLC0t8e+//0JfX7/Wz00NeY247t27h2nTptHerqikfijI433KPl8Kl7hqCL5AUP0emPLdd99BV1cXt27dYrSfuigpKcHExOSrgWnIa0T1+vVr/Pvvv7CysqK9bVFJfbCqNkjLZ/hhQaZ8LCqCjjazS7ZxuVwMGTIEUVFRjPYjrW7dugUVFRVaL4aIS+qD1blzZwDA8zfS89h1Yzx/kw6j/70HJtna2uLmzZtSM6VHksLDw2FlZSU1Fy4AGQiWgYEBdFu0QEwCfTcTJel2UgJ6mZkx3k/V6kzh4eGM9yVNPnz4gMjISKn6fAXIQLA4HA5G2doiNDaG7VIa7fX7HDx4+Ry2traM96WtrY1x48bh9OnTjPclTYKDg6GiooKJEyeyXUoNUh8sAHBxcUH0s3gkpKeyXUqjeF+5gBbNm8PGxkYi/c2ePRt37txBvASWW5MGAoEAR44cgZOTEyMXRcQhE8GysbGBqYkJNpw4ynYpDfa+IB97ws5i/oIFjD3c91/W1tYwMzPD7t27JdIf20JCQpCWloZVq1axXcoXZCJYXC4Xe/ftQ3D0P7gad4/tchrkR58jUNPUwJo1ayTa7/r163HhwgUkJiZKtF9JEwgE2LNnD6ZOnYouXbqwXc4XZGZ/LACYMH48kuOf4u6ug1CXoitA/xWT+BRDVi+Bj68PXFxcJNq3UChE3759oa6uTutjGdLG29sbGzduRHx8PExMpGe73CoyccaqsnffPrwvKYLLji0QUrWvFsS2tOws2P26AaNH28LZ2Vni/XO5XBw8eBDR0dFyG6ycnBz89ttvWLlypVSGCpCxMxYAxMTEYPiwYfh+7ET87r6Q7XJqyC8uhvmaRVDQ1MCt6GhWd/2YN28ezp07h5s3b6JFixas1cGEOXPmIC4uDgkJCYys9UEHmTpjAcCgQYPgffQodoUEYe7e31FJ4+Pd4kjNysTg1YuQV16G8PPnWd9KZ/v27VBXV8fChQvrXAtQFvn4+CAsLAze3t5SGypABoMFAM7OzggJCYF/VARsf16L9wX0PeQniqin8ei/YgEUtTRxJzZWKp4J0tHRQUBAAG7duoW9e/eyXQ4tnj17hp9++gk//vgjRowYwXY5XyVzQ8HPPX78GOPHjUNRfgE2T5uFebbjwZPgDupZH/Pww/HDOH79EsaPG4cTfn6sn6n+a8+ePVixYgW8vLyk7iZqY7x58wajR4+Gqakprl69CgUJ/ncWhUwHC/i09JeHhwd279oFo7btsXzCZDgOsYKmKnM3DFMy3+LI5QvYf+EcdJo3w47ff4ejo/SuSbhy5Urs27cPJ0+elKoZ4A2Vm5uLcePGQUlJCTdv3kSzZs3YLqleMh+sKi9fvsTmzZsR9L8NyAZ1+xZmnY3RroUuLVus5hYW4NW7t7j9PAHPUlPQtk0bLPz+eyxfvlzq7vr/l1AoxPTp0xEaGorjx49/dUkzaZOTk4OpU6ciPz8f0dHRaNeuHdslNYjcBKvKhw8fcPHiRURERCD+0SNkZWWjpLRE7HZVVFRgbGyMPn37YtSoUbC0tGT8OSs6VVZWYtasWQgKCsKff/6JSZMmsV1SvdLS0uDo6Agul4srV66gU6dObJfUYHIXLCbk5eWhTZs22L9/P9zd3dkuR2QURWHlypXYs2cPVq9ejRUrVkj0Uf7GiIqKwvz586Gvr48LFy5Ur80uK6TzqEqZZs2aYdKkSfD29ma7FLFwOBzs3LkTe/bswe7du+Ho6IicnBy2y6pBIBDA09MTDg4OGDJkCCIjI2UuVAAJVoO5u7vj9u3bePbsGduliG3RokWIjo7G69evMXToUAQEBDC+62RDPHnyBGPHjsW+ffuwd+9eBAUFSc06gY1FgtVAw4cPR+fOnWX+rFXFzMwMcXFxmDJlCpYtW4YJEyaw9rhJbm4u1q9fD2traygrK+Pu3btYuFC6ZtU0FglWA3E4HMycORO+vr5Ss/C+uLS0tLBv3z7cvXsXQqEQI0aMgLOzM+7dk8wTBFlZWdi4cSN69eqFsLAwHD58GFFRUejevbtE+mcSuXjRCO/evYOBgQH8/PwwZcoUtsuhFUVRuHjxIrZu3YqYmBj07t0bDg4OsLOzo3Vv44qKCkRERCAoKAhXrlxBixYtsGrVKsydO1eqpyg1FglWI40fPx5lZWW4evUq26UwJiAgAHPnzoVQKERFRQX69+8PCwsLmJubo2fPntVLTDfUq1evcOvWLURFRSEqKgofP37E0KFDMWPGDDg5OUnsQVBJIsFqpNDQUEyaNAkvX76sXkFK3qxfvx4+Pj5ITEzExYsXcfnyZUREROD169fg8XgwMDBA586d0aFDB2hoaEBDQwPa2tooLi6u/vPmzRukpKQgOTkZJSUl0NDQgIWFBYYPHw4HBwcYGNS+zLe8IMFqJD6fD0NDQ7i7u2Pz5s1sl0O7yspKGBoaYu7cudi0aVONf3v58iUePnyIFy9eIDExEWlpaSgsLERhYSE+fvxYHTINDQ20bdsWJiYmMDY2xjfffIM+ffrI1A1IAQGHAAAgAElEQVR1cZFgieCHH36Ar68v0tLSpH4yaGMFBgbC2dkZr169gqGhIdvlyCwSLBGkpKTAyMgI4eHhGDNmDNvl0Gr48OHQ0NBAaGgo26XINBIsEQ0bNgza2toICQlhuxTavHr1Cl26dMH58+cxevRotsuRaeQ+lojc3d1x/vx5vH37lu1SaPPXX39BX18fI0eOZLsUmUeCJSJ7e3toaWnRvs8TW8rLy+Hr64u5c+fK3edGNpBgiUhFRQUuLi44fPiwVMyzE9eZM2eQl5eHWbNmsV2KXCCfscTw5MkTfPfdd4iMjJSphwdrM2TIELRs2bLGlqaE6MgZSwzdu3dH3759ZX5ibmJiIm7duoV58+axXYrcIMES0+zZs6uHUbLq4MGD6NixI4YPH852KXKDBEtMzs7OUFRUxMmTJ9kuRSSlpaXw8/PDvHnzpPZpYllEjqSYNDQ04ODggMOHD7NdikhOnz6NoqIizJw5k+1S5Aq5eEGDmJgYDB48GPfu3UOfPn3YLqdRBg4ciA4dOsDf35/tUuQKOWPRYNCgQfjmm29k7iJGfHw87ty5Qy5aMIAEiyZubm44efIkioqK2C6lwQ4ePAgTExMMHTqU7VLkDgkWTVxdXVFRUSEz94GKiopw8uRJzJs3DxwOh+1y5A4JFk1atGiBCRMm4MiRI2yX0iD+/v4oLy+Xut3m5QUJFo3c3d0RHR2NhIQEtkupl5eXFxwdHaGnp8d2KXKJBItG1tbW6Ny5M44ele5NyO/fv48HDx6QixYMIsGiEYfDwYwZM3D8+HGpXiLNy8sLXbt2xaBBg9guRW6RYNHMzc0NHz9+RFhYGNul1KqwsBCnT5/GwoULyUULBpFg0ax9+/YYOXKk1N7TOnHiBPh8PlxcXNguRa6RYDFg9uzZuHbtGtLS0tgu5QtHjhyBk5OTTGzeJstIsBgwbtw4tGrVCseOHWO7lBpu376Nhw8fkosWEkCCxQAej4cZM2bA29sbAoGA7XKqeXl5oUePHujXrx/bpcg9EiyGuLm54c2bN1KzFPXHjx8RFBSEBQsWsF1Kk0CCxRBjY2MMGTJEai5i+Pj4gMvlwsnJie1SmgQSLAa5u7sjNDQUmZmZbJeCw4cPw8XFBVpaWmyX0iSQYDFo8uTJ0NTUxIkTJ1it4+bNm3j27Bnmzp3Lah1NCXnQkWGLFi3CtWvXkJSUxNoNWRcXFyQnJyM2NpaV/psicsZi2Ny5c/HixQtERUV98W9lZWW09lXb78gPHz4gODiYXGKXMBIshn333XcwMzOrvoghEAhw/vx5jBs3jvZdIVevXo1Zs2bVODMdO3YMysrKcrcDpdSjCMYdOHCAUlFRoVauXEm1atWKAkBxOBzK3Nyc1n6mT59OcTgcCgDVrVs36sCBA5SRkRG1aNEiWvsh6td0dgJjQXl5OcLCwhAcHIyysjLs2bMHfD4fwKdhW2lpKa39FRYWVg8HExMTsXjxYgiFQqSmpiIuLg69e/emtT+ibmQoyICEhAQsW7YMrVq1wtSpUxEZGQkA1aGqUlxcTGu/Hz9+rP7/FEVBIBCAoihcuXIFZmZm+O6773Do0CGZWpdDVpFgMSAtLQ1//vkn8vPzIRQK65zWxMQZqzaVlZUAgKdPn2LevHnYuXMnrf0SXyLBYoCtrS0OHjxY7+vovipYV7CqcLlcTJ06FRs2bKC1X+JLJFgMmT17NpYvX/7VvaboDtbXhniKioro378/jh8/Th5wlAByg5hBQqEQkyZNwsWLF7/4fAUAysrKtIarWbNmNT5nVVFUVESHDh0QGxtLnsOSEBIshpWWlsLCwgLx8fHVn3WqcDgcCAQC2s4gysrKqKioqPE1Ho8HHR0d3L9/H4aGhrT0Q9SPDAUZpqqqivPnz0NPTw88Xs27GxRFfREEUQkEgi/a4nK5UFRUxNWrV0moJIwESwJat26Na9euQUVF5YutckpKSmjpo7bPVxwOByEhIejVqxctfRANR4IlId26dat1+Wm6LrnXFqx9+/Zh5MiRtLRPNA4JlgSNHDkShw4dqvE1uoL1+aV2LpeLn3/+mTwtzCISLAlzd3fHqlWrqv9O11CwahYHh8PBtGnT8PPPP9PSLiEaEiwWbN++HRMmTABA/1DQysoKR44cIfeqWEaCxQIulwt/f38MGDCA1mD16NEDISEhUFRUpKVNQnTkPhZL3r9/jxs3biAlJQU6OjpitcXlcpGZmQkzMzNYWVlBRUWFpiql0/v37/H8+XNkZWWhpKQEJSUl0NHRgZqaGvT19WFsbAxVVVVWaySPjUjQ69evceTIEQQHB+PJkycAAAUFBWhoaIjVrlAorL54oaysjKFDh8LFxQWOjo5yEbLU1FSEhYUhMjISt27dwvv376v/TUlJCWpqajVmnHC5XBgZGcHKygrDhg3D2LFjoaamJtGayRlLAvLy8rBp0yYcOHAA2trasLa2xsCBA9G1a1c0b96clj6EQiEyMjIQHx+PqKgoREVFoXnz5ti+fTtmzJghc5+5+Hw+AgMDcejQIdy8eRPa2toYNGgQBg8eDBMTExgZGaFVq1Y1brqXlpYiPT0dL1++RHx8PG7duoWHDx9CVVUV9vb2WLJkicTu6ZFgMezo0aNYs2YNAGDBggUYM2aMRD4DffjwAd7e3jh79izMzMxw6NAh9OjRg/F+xUVRFHx8fODh4YG0tDTY2tpiypQpGDZsmEjHLTc3F8HBwTh16hSePHkCW1tb/Prrr4wHjASLIXw+H4sXL8ahQ4fg5OSEOXPmiD3kE0VycjI8PT2RlJQEPz8/TJo0SeI1NFR8fDzmz5+Pe/fuwdnZGUuWLKF1KlZERAR27NiBhw8f4vvvv4eHhwc0NTVpa/9zJFgMKCgogL29PaKjo7FlyxZYWlqyWo9AIMCOHTsQHByMbdu2YfXq1azWU5v9+/dj5cqV6NGjBzw9PfHNN98w0g9FUQgICMDmzZvRrFkznD59mpGzFwkWzfh8PsaMGYO4uDjs2bMHJiYmbJdUzd/fHzt37sTBgwelZvHOyspKuLm5wd/fH6tWrar3GTa6ZGdnV58djx07hqlTp9LaPrkqSLNly5bh5s2b8PLykqpQAYCTkxMKCwuxaNEidOrUCSNGjGC1npKSEtjb2+PWrVsICAiQ6Jm9ZcuWCAoKwqZNm+Di4oKcnBwsXryYtvZJsGh04sQJ/PXXX9i2bRtjQxlxzZkzB+np6XBwcEBiYiJat27NSh2VlZWYPHky7t27h5CQEPTs2VPiNSgoKGDLli3Q09PD0qVLoaqqitmzZ9PSNhkK0qSgoADGxsYYOnRo9VVAaVVeXg4HBwfY2Niwtjmeq6srzp49KzWPtXh6emLnzp04d+4cxo4dK3Z7ZEoTTbZs2YKysjKZWMpZWVkZS5YsgY+PDyvruXt5ecHPzw/Hjh2TilABwJo1azBlyhTMmDGDli1uyRmLBtnZ2TAwMMDSpUvh6OjIdjkNNnfuXOjq6uLKlSsS6zMhIQFmZmZYuHAh1q9fL7F+G6KsrAyjRo2CtrY2oqKivngotTHIGYsGPj4+UFZWxvjx49kupVFcXFxw/fp1pKamSqQ/iqKwcOFCmJqaSuVwWUVFBX/99Rfu3bv3xXNzjUWCRYOQkBCZnPxqbm4ODQ0NhIaGSqS/gIAAREVFYceOHRK5pC6Krl27Yu7cufjhhx+Ql5cncjskWGIqLS3F3bt3MWDAALZLaTQFBQX07dsXN27cYLwvoVAIDw8PTJ48mZUrgI2xcuVKAMDevXtFboMES0wJCQkQCARSd8+qoYyNjatn2jMpLCwMSUlJWLp0KeN9iUtTUxNz5szB3r17RV5fnwRLTFX7C+vp6bFciWj09PQkskfy4cOHMWzYMBgbGzPeFx3c3d1RVFSE4OBgkb6fBEtMVWtWyNrnqyqqqqq0rbtRl8zMTFy9elWmNr9r3rw5RowYAV9fX5G+nwRLTFV3K2TteafPMX3H5dKlS1BUVMSoUaMY7YduEydOxI0bN+rdbKI2JFgE4yIiItCvXz9GzuqRkZG4du0a7e0CgIWFBQQCQa37R9eHBItgXHR0NAYOHEhrm//88w8cHR3h6OiIR48e0dp2FV1dXRgbGyM6OrrR30sm4RKMKisrQ3p6OkxNTWltd8CAAejUqRPj27+amJggKSmp0d9HzlhS5tGjRzh69OgXX8/MzMS2bdtYqEg8ycnJEAgE6Ny5M63tKisrS2RmvpGREQmWPOjZsydyc3Ph7e1d/bXMzEz89NNPcHZ2ZrEy0eTk5ABg5naEJGZv6Onp4cOHD43+PhIsKbRq1Srk5eXB29u7OlQbN26EgYEB26U1WtUKverq6rS3XXUllskrshoaGuSqoDxZtWoV3r59i7lz58psqID/31j8v3uDyQpFRUWR9jAjwZJSmZmZSE1NRe/evXH9+nW2yxFZ1ZmKrqW0Ja24uFik1bVIsKRQZmYmfvzxR2zcuBGbNm1Cbm5urRc0ZEHV8mIFBQUsVyKawsJCaGlpNfr7SLCkzOehqlpTb9WqVTIbrqr3kJGRwXIloklLSxNpbUMSLCkjFAprhKrKqlWraL8XJAlt27aFpqYmXr58SXvbVVOxmJyS9erVK5GeXCDBkjJt27at8zfkoEGDJFyN+DgcDr777js8fPiQ9rarLowwNYlYIBDg8ePHIi3NTYJFMM7KykqkaUFfc+/ePWzcuBEAcOHCBRw9ehR8Pp/WPuLj45Gfnw8rK6tGfy8JFsG44cOHIyUlBcnJybS12bdvX2zfvh05OTm4e/cu3NzcaL+kf/36dbRt2xbdunVr9PeSYImpagcMgUDAciWiEQgEjN9jGjJkCPT19REUFMRoP3Q7e/Yspk6dKtINaBIsMWlrawOASHfnpUFhYaHYO0rWh8vlwtnZGQEBAdWfi6RddHQ0Xr16hWnTpon0/SRYYqqaXErHIo9sSE1NhZGREeP9fP/993j//j3OnDnDeF902L17NywtLUVeUJQES0wGBgbQ1dXF48eP2S5FJE+fPmX80QsA0NfXx7Rp07B7926RpghJUmxsLG7cuIEff/xR5DZIsMTE4XBga2uLmzdvsl1Ko2VlZSEhIQG2trYS6W/Tpk3Izs7Gn3/+KZH+RCEQCLB+/XpYW1uLtRsLCRYNXFxc8OjRI6SkpLBdSqOEhoZCV1cXNjY2EulPX18fGzZswO7du/H8+XOJ9NlY+/btw4sXL7B//36x2iHBooG1tTW6deuGv/76i+1SGiw3NxcBAQFYuHAhlJSUJNbv8uXL0atXL8yePVvqJubevXsX27dvx9atW9GlSxex2iLBogGXy8XevXsRGRmJmJgYtstpkAMHDkBTU1Pi26YqKirC398f2dnZWLRokdTcpsjIyICbmxtsbW2xfPlysdsjwaLJ8OHDMXHiROzatYvxdfrE9ejRI4SFhWHHjh2MPIBYHwMDA4SEhODq1atYu3Yt48uv1ef9+/dwcHBA27Zt4efnR8uDk2QbHxplZGSgT58+MDExwR9//CHWNjBMefv2LWbOnAkLCwuEhISwuh7iuXPn4OjoCHt7e+zatYuVhyHT09Ph6OgIDoeDqKgo2tbRkL7/8jJMX18foaGhuHv3Lnbt2sV2OV8oKCjAsmXLYGBggJMnT7K+yOjEiRMRFhaG8PBwuLi4IDc3V6L9x8TEwNbWFtra2rh16xati9OQYNFswIAB8PHxQWBgIDZv3iw1Mw1ev36N2bNno6KiAuHh4awMAWszatQo3LhxAykpKbC0tJTIbYvKykr8/vvvsLOzw+DBgxEZGYlWrVrR2gcJFgOmTJmCsLAwREREYPHixRL/TfxfDx48wKxZs6Cjo4PY2Fi0b9+e1Xr+q0+fPoiLi8OgQYNgb2+PefPm4e3bt4z09c8//8DS0hJ79+7Fzp07cfbsWZGeEK4PCRZDRo8ejZiYGLx//x6TJ0/GqVOnaH+soT45OTn4+eefMX/+fAwbNgxRUVFo27atRGtoqGbNmuHMmTMICwvDw4cP0a9fP6xatQqvXr0Su22hUIi///4b48aNw+TJk2FiYoJnz55hyZIljA2HycULhhUXF+O3337D77//jrZt28LJyQk2NjYiLVDSUOnp6Th37hzOnj0LPT09/PHHH7Czs2OsP7pVVFTg+PHj2L59O1JSUtC3b19MnDgRQ4cObfDTvOXl5bh37x6uX7+O4OBgvHv3Dra2tvjxxx8xePBght8BCZbEpKSkYMuWLfD394dAIECPHj1gYmICPT09sT/vCIVCFBQUICMjA0+ePEFqairat2+PRYsWYcmSJVBVVaXpXUiWQCCAmZkZtLW18fjxY+Tn50NPTw+mpqbo3LkzWrVqBXV1daipqSE/Px9FRUVIT0/Hq1ev8Pz5c5SWlqJLly6YMmUKpk+fLtG9uUiwJCwvLw+XL19GREQEHj9+jKysLLFXMOJwOFBQUECPHj3Qp08fjBo1ChYWFlK7z29D3b17F/3790dsbCzMzMwQFxeHqKgoJCUl4cWLF8jKykJRURGKi4uho6MDDQ0NGBgYwNTUFN27d4eVlRVr6zGSYMmBpKQkdO3aFX///TeGDRvGdjm0mTdvHmJiYiSylSvdyMULOWBqaooBAwbUWO9d1pWWliIwMBDu7u5slyISEiw54e7ujuDgYOTl5bFdCi0CAwNRUlICFxcXtksRCQmWnJg6dSp4PB5OnTrFdim0OHr0KMaPHy+zm6aTYMkJDQ0NTJ48WSZXy/2vlJQUREVFyewwECDBkivu7u6Ii4tjZHFMSTp8+DDatWsHa2trtksRGQmWHDE3N0fXrl1l+qzF5/Ph6+uLWbNmyfTtAhIsOePq6go/Pz+pezq3oS5duoR3797B1dWV7VLEQoIlZ2bOnIni4mKEhoayXYpIjh49CisrK9r3LJY0Eiw506pVK9ja2srkPa2srCxcuHBBpi9aVCHBkkPu7u74+++/aZkZLkm+vr5QU1PDxIkT2S5FbCRYcmj06NFo3bo1Tpw4wXYpjXLs2DFMmzYNampqbJciNhIsOcTj8TB9+nR4e3tLzSpI9YmJiUFiYiLc3NzYLoUWJFhyyt3dHW/evMHff//NdikN4u3tje+++04iy11LAgmWnDI2NsbgwYNl4iJGUVERgoKCMGfOHLZLoQ0Jlhxzc3PDuXPnkJOTw3YpX3X69GmUl5dj6tSpbJdCGxIsOebo6AgVFRWpn5h79OhR2NnZQVdXl+1SaEOCJcfU1dXh6Ogo1cPB58+f4/bt23Jx7+pzJFhyzt3dHU+ePMH9+/fZLqVWR44cgaGhoVw9+QyQYMm9AQMGoFu3blJ51uLz+fDz88PMmTOlcjluccjXuyFq5ebmBn9/f6nbrOH8+fPIzs7GzJkz2S6FdiRYTYCrqyvKysoQHBzMdik1eHt7Y8SIETA0NGS7FNqRYDUBurq6GDNmjFQNBzMzM3H58mW5u2hRhQSriXBzc8M///yD5ORktksB8GleoJaWFiZMmMB2KYwgwWoiRo0ahfbt2+P48eNslwLg00z2adOmQVlZme1SGEGC1UQoKChg+vTpOH78OOsTc2/evImkpCTMmjWL1TqYRILVhMyePRtv377FlStXWK3D29sbffr0Qc+ePVmtg0kkWE1Ix44dMXToUFYvYhQUFODMmTNy83hIXUiwmhg3NzeEh4cjOzublf4DAgJAUZRcTbitDQlWEzN58mSoq6vDz8+Plf69vb1hb2+PZs2asdK/pJBgNTGqqqqYOnUqDh8+XP01iqIQGRmJ6dOn07br5NWrV7F69WokJiZWf+3p06e4e/eu3N67+hzZxqcJun//Pvr27YvQ0FDEx8fj8OHDSE9PBwAUFhbSstukj49P9VSlvn37Yv78+YiLi8PFixfx6tUrxrYolRY8tgsgJKuiogJpaWnQ1NTExIkTwePxUFlZWf3vZWVltASrrKwMPB4PfD4fDx48qH46uGfPnrh9+zYGDRokdh/SjAwFm4ikpCSsW7cObdq0gYODA0pKSkBRVI1QAZ/27qVDWVlZ9Yx1oVBY/efJkycYPHgwOnfujO3btyMrK4uW/qQNOWM1AQkJCejVqxf4fD6EQiEA1HmTmK6lqcvKymod7lUFOSUlBevWrcPZs2cRExMDHk++fhTJGasJqHoeqyEfp8vKymjps752FBQUoKenh+DgYLkLFUCC1WRMmzYNGzZsqPeBQkkEq2oz8vPnz6N9+/a09CdtSLCakE2bNlXv/FgXOoP1tTNkQEAA+vXrR0tf0ogEqwnhcDjw9vZG7969oaioWOtrmA4Wh8PB9u3bMWnSJFr6kVYkWE2MiooKLl68iLZt29Z65qIzWFUXSqooKCjA1dUVq1evpqUPaUaC1QS1aNECly5dgoqKyhefuZgKlqKiIvr37w8vLy9a2pd2JFhNVNeuXREeHl4jWFwul7ZgVd0nAz5t0mBgYIDz589DSUmJlvalHQlWE2ZpaYmDBw9W/53uYAGfhn/q6uq4dOmS3E+8/RwJVhPn7u6OlStXgsvlQiAQ0H65ncvl4vz58+jSpQst7coKEiwCnp6eGDNmDCiKoi1YpaWl4HA4OHr0KMzNzWlpU5aQYBHgcrnw9/dHr169aAuWQCDAhg0bMG3aNFrakzXyN5eEEElpaSnWr1+PK1eu4NChQ2K1xeVyYWRkhHHjxqGsrAwqKio0VSk7yPNYTdjr169x5MgRBAcH48mTJwA+XWwQ97ERoVCIwsJCAICysjKGDh0KFxeX6m2FmgISrCYoLy8PmzZtwoEDB6CtrQ1ra2sMHDgQXbt2RfPmzWnpQygUIiMjA/Hx8YiKikJUVBSaN2+O7du3Y8aMGXL/oCMJVhNz9OhRrFmzBgCwYMECjBkzps7pTXT68OEDvL29cfbsWZiZmeHQoUPo0aMH4/2yhQSrieDz+Vi8eDEOHToEJycnzJkzh5YnhRsrOTkZnp6eSEpKgp+fn9zOGSTBagIKCgpgb2+P6OhobNmyBZaWlqzWIxAIsGPHDgQHB2Pbtm1yOXeQXBWUc3w+Hw4ODnj06BGOHDkCExMTtkuCgoIC1q1bB0NDQ6xduxba2tqYO3cu22XRigRLzi1btgw3b96El5eXVITqc05OTigsLMSiRYvQqVMnjBgxgu2SaEOGgnLsxIkTmDlzJrZt2ya1e/xSFIUNGzbgzp07SExMROvWrdkuiRYkWHKqoKAAxsbGGDp0aPVVQGlVXl4OBwcH2NjY4NixY2yXQwsypUlObdmyBWVlZZg3bx7bpdRLWVkZS5YsgY+PD2JjY9kuhxbkjCWHsrOzYWBggKVLl8LR0ZHtchps7ty50NXVZX2bITqQM5Yc8vHxgbKyMsaPH892KY3i4uKC69evIzU1le1SxEaCJYdCQkJgZWUlc/PyzM3NoaGhgdDQULZLERsJlpwpLS3F3bt3MWDAALZLaTQFBQX07dsXN27cYLsUsZFgyZmEhAQIBAKpu2fVUMbGxtUz7WUZCZacyczMBADo6emxXIlo9PT0qt+DLCPBkjNVi7jI2uerKqqqqtXvQZaRYMmZqrsnsvy8kzzcASLBIggGkGARDSYUClFQUMB2GTKBBItosKysLOzbt4/tMmQCCRZBMIAEiyAYQIJFEAwgTxATdQoLC0NSUlL134uLi5GYmAhPT88ar3N3d0eLFi0kXZ5UI8Ei6jRgwAB8++231X9///49ysrKMHny5Bqv09LSknRpUo8Ei6hTy5Yt0bJly+q/q6qqQktLC506dWKxKtlAPmMRBANIsAiCASRYRIMpKyujc+fObJchE0iwiAZr3rw5pk6dynYZMoEEiyAYQIIlZ6p2DhEIBCxXIhqBQAAeT/YvVpNgyRltbW0AqN74TdYUFhZCR0eH7TLERoIlZ6ouLqSlpbFciWhSU1NhZGTEdhliI8GSMwYGBtDV1cXjx4/ZLkUkT58+Re/evdkuQ2wkWHKGw+HA1tYWN2/eZLuURsvKykJCQgJsbW3ZLkVsJFhyyMXFBY8ePUJKSgrbpTRKaGgodHV1YWNjw3YpYiPBkkPW1tbo1q0b/vrrL7ZLabDc3FwEBARg4cKFUFJSYrscsZFgySEul4u9e/ciMjISMTExbJfTIAcOHICmpqbcbJtKgiWnhg8fjokTJ2LXrl1Sv07fo0ePEBYWhh07dkBdXZ3tcmhBtvGRYxkZGejTpw9MTEzwxx9/gMuVvt+jb9++xcyZM2FhYYGQkBCZXg/xcyRYcu7OnTuwtLSEnZ0dVq5cyXY5NRQUFGD27NnQ0tLCrVu35OZsBZChoNwbMGAAfHx8EBgYiM2bN6OyspLtkgAAr1+/xuzZs1FRUYHw8HC5ChVAgtUkTJkyBWFhYYiIiMDixYuRm5vLaj0PHjzArFmzoKOjg9jYWLRv357VephAhoJNyJMnTzB+/Hjk5uZi9uzZcHR0lOiE15ycHPz555+4ePEi7Ozs4OvrCzU1NYn1L0kkWE1McXExfvvtN/z+++9o27YtnJycYGNjAw0NDcb6TE9Px7lz53D27Fno6enhjz/+gJ2dHWP9SQMSrCYq5f/au7eQqNY3DOCP456pacpJw1NNhRoapqhoaWVXaYRBVFbWeCiUvJIossuIkgq6Fb3YRHRAwsqrgiwpK8vqIiIzE0ItdYKgAzMypjNN7774/5PtVsfjN6ee391a3/L1XYOP61trDWt1d6OqqgrXrl2Dy+VCSkoKEhISEB4ePuvznd/PeO/r68ObN2/w4cMHmEwmVFRU4PDhw9Dr9XO0F76LwfrDff/+HY2NjXjw4AFev36Nz58/u33xgYhARNxeutdoNFi8eDFiY2ORnp6OrVu3YtOmTQgODlaxCz6JwaJpuX79OgoKCgLiHVYq8aogkQIMFpECDBaRAgwWkQIMFpECDBaRAgwWkQIMFpECDBaRAgwWkQIMFpECDBaRAgwWkQIMFti+f0wAAAXaSURBVJECDBaRAgwWkQIMFpECDBaRAgwWkQIMFpECDBaRAgwWkQIMFpECDBaRAgwWkQIMFpECDBaRAgwWkQIMFpECDBaRAp57Tyb5nW/fvuH+/fuj1j1//hwAcOPGjVHrDQYD8vLyPNabr+P7sWhCQ0NDiIiIwMDAwKTbFhUV4erVqx7oyj9wKkgTmj9/Pnbt2gWdTjfptmaz2QMd+Q8Gi9wym81wOBxutzEajcjJyfFQR/6BwSK3Nm/ejLCwsAnHtVotzGYztFqtB7vyfQwWuRUcHIzCwsIJp4NOpxP79+/3cFe+jxcvaFLPnj3Dhg0bxh2LioqCxWKBRsP/0f/GT4MmlZWVBZPJNGa9TqdDSUkJQzUOfiI0qaCgIBQXF485j3I4HJwGToBTQZqSjo4OrFmzZtS62NhYdHV1eakj38YjFk1JYmIiEhISRpa1Wi0OHjzovYZ8HINFU1ZSUjIyHXQ6ndi3b5+XO/JdnArSlPX09CAuLg4igtTUVLx69crbLfksHrFoymJiYpCeng4AOHDggJe78W0MFk3L78vre/fu9XYrPo3BomkpKChAbm4uli5d6u1WfBrPsWjaOjs7sXr1am+34dMYLCIFOBUkUoDBIlKAwSJSgA+TCXDv3r3D48eP0d7ejrCwMGRkZCAnJwd6vd7brQU0HrEClN1ux9GjR1FUVIS4uDicPHkSO3fuRHNzM9LT02f8rYnh4eE57tQztT1OKCDl5eXJqlWrZHBwcMzY6dOnRafTyYsXL6Zd99ixY+JyueaiRY/W9jQGKwDV1NQIALl06dK44zabTUJDQyU5OVkcDseU67a1tYnBYFDyx6+ytjfwPlYAioiIwNevX/Hjx48Jn1VRVlaGixcvoq6uDsHBwfj16xe0Wi12794NALh58yacTif0ej127NiBp0+fwmw2o7e3F3V1ddBqtdizZw+6urpw69YtHDlyBE+ePMGdO3cQHx+P4uJiaDQa1NfXz7i2X/N2smluWSwWASDLli1zu92pU6cEgBw/flxsNpts3LhRQkJCRsY/ffokycnJEhUVJSIiLS0tUlhYKADk9u3bcvfuXamurpaFCxdKdHS01NXVSXJysuj1egEg+fn5IiIzru3vePEiwLS1tQEAli9f7na73+MdHR1YtGgR0tLSRo1HR0cjMzNzZDk7Oxvx8fEAgLy8PGzZsgUVFRXYtm0bbDYbRARtbW3o6urC+vXr0dDQgHv37s24tr9jsAKM0WgEANhsNrfbyf/PAJYsWQIA4z4QZioPiTEYDAgJCUFhYSGA/4Xm3LlzAICmpqZZ1fZngb13f6DExEQAwMePH91u19/fDwBISkqa9e8MCgoatbx27VoAQF9f36xr+ysGK8AYjUakpaXBbre7fdBLZ2cnNBoNcnNz57wHnU6HefPmYcWKFXNe218wWAGotrYWQUFBOH/+/Ljj/f39aGhoQEVFBVJTUwEAISEhY27QighcLteYn//vuqGhoVHLra2tGB4exrp162Zd218xWAEoKysLVVVVuHLlCh4+fDhqzGaz4dChQ8jKysKZM2dG1q9cuRLDw8NoamqCiKC+vh6tra2wWq2wWq1wuVwIDw8HALx8+RItLS0jgbJarejt7R2p1djYiIyMDOTn58+6tt/y5iVJUqu5uVlSUlKktLRUqqurpbKyUjIzM+Xs2bNjbsTa7XZJSkoSABIZGSmXL1+W8vJyCQ0NlcrKSvny5Yt0d3dLZGSkhIaGyoULF0REpLS0VAwGg2zfvl1qamqkvLxcsrOzpaenZ9a1/RlvEP8BrFYr3r59C5PJ5Pa8R0TQ3t6OuLg4LFiwAO/fv4fJZBr1hV2n04mfP3+OrCsrK0NjYyN6enrQ0dEBo9GImJiYOantzxgsmpXfwbJYLN5uxafwHItmZXBwEHa73dtt+BwGi2bE6XSitrYWjx49wsDAAE6cODFyb4w4FSRSgkcsIgUYLCIFGCwiBRgsIgX+AvC3t5sgCjT/AJshKMnD8xCjAAAAAElFTkSuQmCC"
+ },
+ "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