# Extensions **Concrete** supports native Python and NumPy operations as much as possible, but not everything in Python or NumPy is available. Therefore, we provide some extensions ourselves to improve your experience. ## fhe.univariate(function) Allows you to wrap any univariate function into a single table lookup: ```python import numpy as np from concrete import fhe def complex_univariate_function(x): def per_element(element): result = 0 for i in range(element): result += i return result return np.vectorize(per_element)(x) @fhe.compiler({"x": "encrypted"}) def f(x): return fhe.univariate(complex_univariate_function)(x) inputset = [np.random.randint(0, 5, size=(3, 2)) for _ in range(10)] circuit = f.compile(inputset) sample = np.array([ [0, 4], [2, 1], [3, 0], ]) assert np.array_equal(circuit.encrypt_run_decrypt(sample), complex_univariate_function(sample)) ``` {% hint style="danger" %} The wrapped function: - shouldn't have any side effects (e.g., no modification of global state) - should be deterministic (e.g., no random numbers) - should have the same output shape as its input (i.e., `output.shape` should be the same with `input.shape`) - each output element should correspond to a single input element (e.g., `output[0]` should only depend on `input[0]`) If any of these constraints are violated, the outcome is undefined. {% endhint %} ## fhe.conv(...) Allows you to perform a convolution operation, with the same semantic as [onnx.Conv](https://github.com/onnx/onnx/blob/main/docs/Operators.md#conv): ```python import numpy as np from concrete import fhe weight = np.array([[2, 1], [3, 2]]).reshape(1, 1, 2, 2) @fhe.compiler({"x": "encrypted"}) def f(x): return fhe.conv(x, weight, strides=(2, 2), dilations=(1, 1), group=1) inputset = [np.random.randint(0, 4, size=(1, 1, 4, 4)) for _ in range(10)] circuit = f.compile(inputset) sample = np.array( [ [3, 2, 1, 0], [3, 2, 1, 0], [3, 2, 1, 0], [3, 2, 1, 0], ] ).reshape(1, 1, 4, 4) assert np.array_equal(circuit.encrypt_run_decrypt(sample), f(sample)) ``` {% hint style="danger" %} Only 2D convolutions without padding and with one group are currently supported. {% endhint %} ## fhe.maxpool(...) Allows you to perform a maxpool operation, with the same semantic as [onnx.MaxPool](https://github.com/onnx/onnx/blob/main/docs/Operators.md#maxpool): ```python import numpy as np from concrete import fhe @fhe.compiler({"x": "encrypted"}) def f(x): return fhe.maxpool(x, kernel_shape=(2, 2), strides=(2, 2), dilations=(1, 1)) inputset = [np.random.randint(0, 4, size=(1, 1, 4, 4)) for _ in range(10)] circuit = f.compile(inputset) sample = np.array( [ [3, 2, 1, 0], [3, 2, 1, 0], [3, 2, 1, 0], [3, 2, 1, 0], ] ).reshape(1, 1, 4, 4) assert np.array_equal(circuit.encrypt_run_decrypt(sample), f(sample)) ``` {% hint style="danger" %} Only 2D maxpooling without padding and up to 15-bits is currently supported. {% endhint %} ## fhe.array(...) Allows you to create encrypted arrays: ```python import numpy as np from concrete import fhe @fhe.compiler({"x": "encrypted", "y": "encrypted"}) def f(x, y): return fhe.array([x, y]) inputset = [(3, 2), (7, 0), (0, 7), (4, 2)] circuit = f.compile(inputset) sample = (3, 4) assert np.array_equal(circuit.encrypt_run_decrypt(*sample), f(*sample)) ``` {% hint style="danger" %} Currently, only scalars can be used to create arrays. {% endhint %} ## fhe.zero() Allows you to create an encrypted scalar zero: ```python from concrete import fhe import numpy as np @fhe.compiler({"x": "encrypted"}) def f(x): z = fhe.zero() return x + z inputset = range(10) circuit = f.compile(inputset) for x in range(10): assert circuit.encrypt_run_decrypt(x) == x ``` ## fhe.zeros(shape) Allows you to create an encrypted tensor of zeros: ```python from concrete import fhe import numpy as np @fhe.compiler({"x": "encrypted"}) def f(x): z = fhe.zeros((2, 3)) return x + z inputset = range(10) circuit = f.compile(inputset) for x in range(10): assert np.array_equal(circuit.encrypt_run_decrypt(x), np.array([[x, x, x], [x, x, x]])) ``` ## fhe.one() Allows you to create an encrypted scalar one: ```python from concrete import fhe import numpy as np @fhe.compiler({"x": "encrypted"}) def f(x): z = fhe.one() return x + z inputset = range(10) circuit = f.compile(inputset) for x in range(10): assert circuit.encrypt_run_decrypt(x) == x + 1 ``` ## fhe.ones(shape) Allows you to create an encrypted tensor of ones: ```python from concrete import fhe import numpy as np @fhe.compiler({"x": "encrypted"}) def f(x): z = fhe.ones((2, 3)) return x + z inputset = range(10) circuit = f.compile(inputset) for x in range(10): assert np.array_equal(circuit.encrypt_run_decrypt(x), np.array([[x, x, x], [x, x, x]]) + 1) ```