Direct circuits are still experimental. It is very easy to shoot yourself in the foot (e.g., no overflow checks, no type coercion) while using direct circuits, so utilize them with care.
For some applications, data types of inputs, intermediate values, and outputs are known (e.g., for manipulating bytes, you would want to use uint8). Using inputsets to determine bounds in these cases are not necessary, or even error-prone. Therefore, another interface for defining such circuits is introduced:
There are a few differences between direct circuits and traditional circuits:
Remember that the resulting dtype for each operation will be determined by its inputs. This can lead to some unexpected results if you're not careful (e.g., if you do -x
where x: fhe.uint8
, you'll fail to get the negative value as the result will be fhe.uint8
as well)
Use fhe types in .astype(...)
calls (e.g., np.sqrt(x).astype(fhe.uint4)
). There is no inputset evaluation, so the bit width of the output cannot be determined.
Specify the resulting data type in univariate extension (e.g., fhe.univariate(function, outputs=fhe.uint4)(x)
), for the same reason as above.
Be careful with overflows. With inputset evaluation, you'll get bigger bit widths but no overflows. With direct definition, you must ensure there aren't any overflows!
Let's review a more complicated example to see how direct circuits behave:
This prints:
Here is the breakdown of assigned data types:
As you can see, %8
is subtraction of two unsigned values, and it's unsigned as well. In an overflow condition where c > d
, it results in undefined behavior.