|
5 | 5 |
|
6 | 6 | # pylint:disable=too-many-lines
|
7 | 7 |
|
8 |
| -from typing import Any, Callable, Optional, Sequence, Union, get_args, overload |
| 8 | +from typing import (Any, Callable, Optional, Sequence, Tuple, Union, get_args, |
| 9 | + overload) |
9 | 10 |
|
10 | 11 | import numpy as np
|
11 | 12 | from nada_dsl import (Boolean, Input, Integer, Output, Party, PublicInteger,
|
|
16 | 17 | from nada_numpy.nada_typing import (AnyNadaType, NadaBoolean,
|
17 | 18 | NadaCleartextType, NadaInteger,
|
18 | 19 | NadaRational, NadaUnsignedInteger)
|
19 |
| -from nada_numpy.types import (Rational, SecretRational, fxp_abs, get_log_scale, |
20 |
| - public_rational, rational, secret_rational, sign) |
| 20 | +from nada_numpy.types import (Rational, SecretBoolean, SecretRational, fxp_abs, |
| 21 | + get_log_scale, public_rational, rational, |
| 22 | + secret_rational, sign) |
21 | 23 | from nada_numpy.utils import copy_metadata
|
22 | 24 |
|
23 | 25 |
|
@@ -776,6 +778,176 @@ def random(
|
776 | 778 |
|
777 | 779 | return NadaArray(np.array(NadaArray._create_list(dims, None, None, generator)))
|
778 | 780 |
|
| 781 | + def shuffle(self) -> "NadaArray": |
| 782 | + """ |
| 783 | + Shuffles a 1D array using the Benes network. |
| 784 | +
|
| 785 | + This function rearranges the elements of a 1-dimensional array in a deterministic but |
| 786 | + seemingly random order based on the Benes network, a network used in certain types of |
| 787 | + sorting and switching circuits. The Benes network requires the input array's length |
| 788 | + to be a power of two (e.g., 2, 4, 8, 16, ...). |
| 789 | +
|
| 790 | + Note: The resulting shuffled arrays contain the same elements as the input arrays. |
| 791 | +
|
| 792 | + Args: |
| 793 | + NadaArray: The input array to be shuffled. This must be a 1-dimensional NumPy array. |
| 794 | + The length of the array must be a power of two. |
| 795 | +
|
| 796 | + Returns: |
| 797 | + NadaArray: The shuffled version of the input array. The output is a new array where |
| 798 | + the elements have been rearranged according to the Benes network. |
| 799 | +
|
| 800 | + Raises: |
| 801 | + ValueError: If the length of the input array is not a power of two. |
| 802 | +
|
| 803 | + Example: |
| 804 | + ```python |
| 805 | + import nada_numpy as na |
| 806 | +
|
| 807 | + # Example arrays with different data types |
| 808 | + parties = na.parties(2) |
| 809 | + a = na.array([8], parties[0], "A", na.Rational) |
| 810 | + b = na.array([8], parties[0], "B", na.SecretRational) |
| 811 | + c = na.array([8], parties[0], "C", PublicInteger) |
| 812 | + d = na.array([8], parties[0], "D", SecretInteger) |
| 813 | +
|
| 814 | + # Shuffling the arrays |
| 815 | + shuffled_a = a.shuffle() |
| 816 | + shuffled_b = b.shuffle() |
| 817 | + shuffled_c = c.shuffle() |
| 818 | + ``` |
| 819 | +
|
| 820 | + Frequency analysis: |
| 821 | +
|
| 822 | + This script performs a frequency analysis of a shuffle function implemented using a |
| 823 | + Benes network. It includes a function for shuffle, a test function for evaluating |
| 824 | + randomness, and an example of running the test. Below is an overview of the code and |
| 825 | + its output. |
| 826 | +
|
| 827 | + 1. **Shuffle Function**: |
| 828 | +
|
| 829 | + The `shuffle` function shuffles a 1D array using a Benes network approach. |
| 830 | + The Benes network is defined by the function `_benes_network(n)`, which should provide |
| 831 | + the network stages required for the shuffle. |
| 832 | +
|
| 833 | + ```python |
| 834 | + import numpy as np |
| 835 | + import random |
| 836 | +
|
| 837 | + def rand_bool(): |
| 838 | + # Simulates a random boolean value |
| 839 | + return random.choice([0, 1]) == 0 |
| 840 | +
|
| 841 | + def swap_gate(a, b): |
| 842 | + # Conditionally swaps two values based on a random boolean |
| 843 | + rbool = rand_bool() |
| 844 | + return (b, a) if rbool else (a, b) |
| 845 | +
|
| 846 | + def shuffle(array): |
| 847 | + # Applies Benes network shuffle to a 1D array |
| 848 | + if array.ndim != 1: |
| 849 | + raise ValueError("Input array must be a 1D array.") |
| 850 | +
|
| 851 | + n = array.size |
| 852 | + bnet = benes_network(n) |
| 853 | + swap_array = np.ones(n) |
| 854 | +
|
| 855 | + first_numbers = np.arange(0, n, 2) |
| 856 | + second_numbers = np.arange(1, n, 2) |
| 857 | + pairs = np.column_stack((first_numbers, second_numbers)) |
| 858 | +
|
| 859 | + for stage in bnet: |
| 860 | + for ((i0, i1), (a, b)) in zip(pairs, stage): |
| 861 | + swap_array[i0], swap_array[i1] = swap_gate(array[a], array[b]) |
| 862 | + array = swap_array.copy() |
| 863 | +
|
| 864 | + return array |
| 865 | + ``` |
| 866 | +
|
| 867 | + 2. **Randomness Test Function:**: |
| 868 | + The test_shuffle_randomness function evaluates the shuffle function by performing |
| 869 | + multiple shuffles and counting the occurrences of each element at each position. |
| 870 | +
|
| 871 | + ```python |
| 872 | + def test_shuffle_randomness(vector_size, num_shuffles): |
| 873 | + # Initializes vector and count matrix |
| 874 | + vector = np.arange(vector_size) |
| 875 | + counts = np.zeros((vector_size, vector_size), dtype=int) |
| 876 | +
|
| 877 | + # Performs shuffling and counts occurrences |
| 878 | + for _ in range(num_shuffles): |
| 879 | + shuffled_vector = shuffle(vector) |
| 880 | + for position, element in enumerate(shuffled_vector): |
| 881 | + counts[int(element), position] += 1 |
| 882 | +
|
| 883 | + # Computes average counts and deviation |
| 884 | + average_counts = num_shuffles / vector_size |
| 885 | + deviation = np.abs(counts - average_counts) |
| 886 | +
|
| 887 | + return counts, average_counts, deviation |
| 888 | + ``` |
| 889 | +
|
| 890 | +
|
| 891 | + Running the `test_shuffle_randomness` function with a vector size of 8 and 100,000 |
| 892 | + shuffles provides the following results: |
| 893 | +
|
| 894 | + ```python |
| 895 | + vector_size = 8 # Size of the vector |
| 896 | + num_shuffles = 100000 # Number of shuffles to perform |
| 897 | +
|
| 898 | + counts, average_counts, deviation = test_shuffle_randomness(vector_size, |
| 899 | + num_shuffles) |
| 900 | +
|
| 901 | + print("Counts of numbers appearances at each position:") |
| 902 | + for i in range(vector_size): |
| 903 | + print(f"Number {i}: {counts[i]}") |
| 904 | + print("Expected count of number per slot:", average_counts) |
| 905 | + print("\nDeviation from the expected average:") |
| 906 | + for i in range(vector_size): |
| 907 | + print(f"Number {i}: {deviation[i]}") |
| 908 | + ``` |
| 909 | + ```bash |
| 910 | + >>> Counts of numbers appearances at each position: |
| 911 | + >>> Number 0: [12477 12409 12611 12549 12361 12548 12591 12454] |
| 912 | + >>> Number 1: [12506 12669 12562 12414 12311 12408 12377 12753] |
| 913 | + >>> Number 2: [12595 12327 12461 12607 12492 12721 12419 12378] |
| 914 | + >>> Number 3: [12417 12498 12586 12433 12627 12231 12638 12570] |
| 915 | + >>> Number 4: [12370 12544 12404 12337 12497 12743 12588 12517] |
| 916 | + >>> Number 5: [12559 12420 12416 12791 12508 12489 12360 12457] |
| 917 | + >>> Number 6: [12669 12459 12396 12394 12757 12511 12423 12391] |
| 918 | + >>> Number 7: [12407 12674 12564 12475 12447 12349 12604 12480] |
| 919 | + >>> Expected count of number per slot: 12500.0 |
| 920 | + >>> |
| 921 | + >>> Deviation from the expected average: |
| 922 | + >>> Number 0: [ 23. 91. 111. 49. 139. 48. 91. 46.] |
| 923 | + >>> Number 1: [ 6. 169. 62. 86. 189. 92. 123. 253.] |
| 924 | + >>> Number 2: [ 95. 173. 39. 107. 8. 221. 81. 122.] |
| 925 | + >>> Number 3: [ 83. 2. 86. 67. 127. 269. 138. 70.] |
| 926 | + >>> Number 4: [130. 44. 96. 163. 3. 243. 88. 17.] |
| 927 | + >>> Number 5: [ 59. 80. 84. 291. 8. 11. 140. 43.] |
| 928 | + >>> Number 6: [169. 41. 104. 106. 257. 11. 77. 109.] |
| 929 | + >>> Number 7: [ 93. 174. 64. 25. 53. 151. 104. 20.] |
| 930 | + ``` |
| 931 | + """ |
| 932 | + arr = self.copy() |
| 933 | + # Ensure the array is a 1D array |
| 934 | + if arr.ndim != 1: |
| 935 | + raise ValueError("Input array must be a 1D array.") |
| 936 | + |
| 937 | + n = arr.size |
| 938 | + bnet = _benes_network(n) |
| 939 | + swap_arr = arr.copy() |
| 940 | + |
| 941 | + evens = np.arange(0, n, 2) |
| 942 | + odds = np.arange(1, n, 2) |
| 943 | + pairs = np.column_stack((evens, odds)) |
| 944 | + for stage in bnet: |
| 945 | + for (i0, i1), (a, b) in zip(pairs, stage): |
| 946 | + swap_arr[i0], swap_arr[i1] = _swap_gate(arr[a], arr[b]) |
| 947 | + arr = swap_arr.copy() |
| 948 | + |
| 949 | + return arr |
| 950 | + |
779 | 951 | def __len__(self):
|
780 | 952 | """
|
781 | 953 | Overrides the default behavior of returning the length of the object.
|
@@ -1535,8 +1707,8 @@ def cossin(self, iterations: int = 10) -> "NadaArray":
|
1535 | 1707 | iterations (int, optional): determines the number of iterations to run. Defaults to 10.
|
1536 | 1708 |
|
1537 | 1709 | Returns:
|
1538 |
| - Tuple[NadaArray, NadaArray]: |
1539 |
| - A tuple where the first element is cos and the second element is the sin. |
| 1710 | + NadaArray: |
| 1711 | + An array of tuples where the first element is cos and the second element is the sin. |
1540 | 1712 | """
|
1541 | 1713 | if self.is_rational:
|
1542 | 1714 |
|
@@ -1922,3 +2094,106 @@ def get_dtype(
|
1922 | 2094 | if all(unique_type in get_args(base_dtype) for unique_type in unique_types):
|
1923 | 2095 | return base_dtype
|
1924 | 2096 | raise TypeError(f"Nada-incompatible dtypes detected in `{unique_types}`.")
|
| 2097 | + |
| 2098 | + |
| 2099 | +# Shuffle |
| 2100 | + |
| 2101 | + |
| 2102 | +def _butterfly_block(base: int, step: int) -> np.ndarray: |
| 2103 | + """ |
| 2104 | + Generates a butterfly block of connections for a given base index and step. |
| 2105 | +
|
| 2106 | + Parameters: |
| 2107 | + base (int): The starting index for the butterfly block. |
| 2108 | + step (int): The step size used to calculate the indices for connections. |
| 2109 | +
|
| 2110 | + Returns: |
| 2111 | + np.ndarray: A 2D array of connections where each row represents a pair of |
| 2112 | + connected indices. |
| 2113 | + """ |
| 2114 | + # Create a range of indices |
| 2115 | + indices = np.arange(0, step, 2) |
| 2116 | + # First half |
| 2117 | + stage_i_1st_half = np.column_stack((base + indices, base + (indices + step))) |
| 2118 | + # Second half |
| 2119 | + stage_i_2nd_half = np.column_stack( |
| 2120 | + (base + (indices + step + 1), base + (indices + 1)) |
| 2121 | + ) |
| 2122 | + # Concatenate the two halves |
| 2123 | + return np.vstack((stage_i_1st_half, stage_i_2nd_half)) |
| 2124 | + |
| 2125 | + |
| 2126 | +def _benes_network(n: int) -> np.ndarray: |
| 2127 | + """ |
| 2128 | + Constructs the Benes network for a given number of inputs/outputs. |
| 2129 | +
|
| 2130 | + Args: |
| 2131 | + n (int): The number of inputs/outputs. Must be a power of 2. |
| 2132 | +
|
| 2133 | + Returns: |
| 2134 | + np.ndarray: A 3D array where each 2D array represents the connections for a stage in the |
| 2135 | + network. Each row in the 2D array represents a pair of connected indices. |
| 2136 | + """ |
| 2137 | + if (n & (n - 1)) != 0 or n <= 0: |
| 2138 | + raise ValueError( |
| 2139 | + f"Benes network generation error. You asked for a benes network on {n} elemenst.\ |
| 2140 | + The number of inputs must be a power of 2 and greater than 0. " |
| 2141 | + ) |
| 2142 | + |
| 2143 | + stages = [] |
| 2144 | + log_n = int(np.log2(n)) |
| 2145 | + |
| 2146 | + # Stage 0: Initial connections (adjacent pairs) |
| 2147 | + indices = np.arange(0, n, 2) |
| 2148 | + stage_0 = np.column_stack((indices, indices + 1)) |
| 2149 | + stages.append(stage_0) |
| 2150 | + |
| 2151 | + # Stages 1 to log_n: |
| 2152 | + for stage_i in range(1, log_n): |
| 2153 | + step = n // 2**stage_i # index step between the first and second half of blocks |
| 2154 | + nr_of_halfs = 2**stage_i |
| 2155 | + stage_i_connections = [] |
| 2156 | + |
| 2157 | + for idx in range(0, nr_of_halfs, 2): |
| 2158 | + base = idx * step |
| 2159 | + halves = _butterfly_block(base, step) |
| 2160 | + stage_i_connections.append(halves) |
| 2161 | + |
| 2162 | + # Combine the connections for the current stage |
| 2163 | + stage = np.vstack(stage_i_connections) |
| 2164 | + stages.append(stage) |
| 2165 | + |
| 2166 | + # Reverse the stages for the second half |
| 2167 | + stages += stages[1:][::-1] |
| 2168 | + |
| 2169 | + return np.array(stages) |
| 2170 | + |
| 2171 | + |
| 2172 | +def _rand_bool() -> SecretBoolean: |
| 2173 | + """ |
| 2174 | + Generates a random boolean. |
| 2175 | + """ |
| 2176 | + r = NadaArray.random((1,), SecretRational)[0] |
| 2177 | + return r > rational(0) |
| 2178 | + |
| 2179 | + |
| 2180 | +_SwapTypes = Union[ |
| 2181 | + Rational, |
| 2182 | + SecretRational, |
| 2183 | + SecretInteger, |
| 2184 | + PublicInteger, |
| 2185 | + Integer, |
| 2186 | + PublicUnsignedInteger, |
| 2187 | + SecretUnsignedInteger, |
| 2188 | +] |
| 2189 | + |
| 2190 | + |
| 2191 | +def _swap_gate(a: _SwapTypes, b: _SwapTypes) -> Tuple[_SwapTypes, _SwapTypes]: |
| 2192 | + """ |
| 2193 | + Conditionally swaps two secret-shared rational numbers using a random boolean value. |
| 2194 | + """ |
| 2195 | + rbool = _rand_bool() |
| 2196 | + # swap |
| 2197 | + r1 = rbool.if_else(a, b) |
| 2198 | + r2 = rbool.if_else(b, a) |
| 2199 | + return r1, r2 |
0 commit comments