pqlattice 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. pqlattice/__init__.py +6 -0
  2. pqlattice/_backends/__init__.py +0 -0
  3. pqlattice/_backends/_fast.py +75 -0
  4. pqlattice/_backends/_native.py +33 -0
  5. pqlattice/_backends/_protocol.py +15 -0
  6. pqlattice/_utils.py +201 -0
  7. pqlattice/integer/__init__.py +8 -0
  8. pqlattice/integer/_integer.py +55 -0
  9. pqlattice/integer/_modintring.py +246 -0
  10. pqlattice/integer/_modring.py +165 -0
  11. pqlattice/integer/_primality.py +78 -0
  12. pqlattice/integer/_primes.py +57 -0
  13. pqlattice/lattice/__init__.py +44 -0
  14. pqlattice/lattice/_bkz.py +87 -0
  15. pqlattice/lattice/_cvp.py +62 -0
  16. pqlattice/lattice/_embeddings.py +149 -0
  17. pqlattice/lattice/_gso.py +43 -0
  18. pqlattice/lattice/_hkz.py +20 -0
  19. pqlattice/lattice/_lattice.py +137 -0
  20. pqlattice/lattice/_lll.py +93 -0
  21. pqlattice/lattice/_svp.py +89 -0
  22. pqlattice/lattice/embeddings.py +3 -0
  23. pqlattice/linalg/__init__.py +37 -0
  24. pqlattice/linalg/_linalg.py +306 -0
  25. pqlattice/linalg/_modint.py +209 -0
  26. pqlattice/linalg/_utils.py +167 -0
  27. pqlattice/polynomial/__init__.py +5 -0
  28. pqlattice/polynomial/_modpolyqring.py +185 -0
  29. pqlattice/polynomial/_modpolyring.py +267 -0
  30. pqlattice/polynomial/_poly.py +250 -0
  31. pqlattice/polynomial/poly.py +3 -0
  32. pqlattice/py.typed +0 -0
  33. pqlattice/random/__init__.py +7 -0
  34. pqlattice/random/_distribution.py +303 -0
  35. pqlattice/random/_lattice.py +53 -0
  36. pqlattice/random/_lwe.py +109 -0
  37. pqlattice/random/_lwr.py +41 -0
  38. pqlattice/random/_prime.py +53 -0
  39. pqlattice/random/distribution.py +3 -0
  40. pqlattice/settings.py +66 -0
  41. pqlattice/typing/__init__.py +4 -0
  42. pqlattice/typing/_types.py +18 -0
  43. pqlattice/typing/_types_validator.py +57 -0
  44. pqlattice-0.1.2.dist-info/METADATA +33 -0
  45. pqlattice-0.1.2.dist-info/RECORD +47 -0
  46. pqlattice-0.1.2.dist-info/WHEEL +4 -0
  47. pqlattice-0.1.2.dist-info/licenses/LICENSE +7 -0
@@ -0,0 +1,303 @@
1
+ import math
2
+ import random
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, override
5
+
6
+ from .._utils import as_integer
7
+ from ..typing import Matrix, Vector
8
+
9
+
10
+ class Distribution(ABC):
11
+ def __init__(self, seed: int | None = None):
12
+ self._pyrng = random.Random(seed)
13
+
14
+ def set_seed(self, seed: int | None) -> None:
15
+ if seed is not None:
16
+ self._pyrng.seed(seed)
17
+
18
+ @abstractmethod
19
+ def sample_int(self, seed: int | None) -> int: ...
20
+
21
+ @abstractmethod
22
+ def sample_vector(self, n: int, seed: int | None = None) -> Vector: ...
23
+
24
+ @abstractmethod
25
+ def sample_matrix(self, rows: int, cols: int | None = None, seed: int | None = None) -> Matrix: ...
26
+
27
+ @abstractmethod
28
+ def set_params(self, *args: Any) -> None: ...
29
+
30
+ @abstractmethod
31
+ def get_params(self) -> dict[str, Any]: ...
32
+
33
+
34
+ class Uniform(Distribution):
35
+ def __init__(self, range_beg: int, range_end: int, seed: int | None = None):
36
+ """
37
+ Creates a uniform sampler from range [range_beg; range_end].
38
+
39
+ Parameters
40
+ ----------
41
+ range_beg : int
42
+ begin of sampling range. Inclusive
43
+ range_end : int
44
+ end of sampling range. Inclusive
45
+ seed : int | None, optional
46
+ seed for random number generator.
47
+ """
48
+ super().__init__(seed)
49
+ self._range_beg = range_beg
50
+ self._range_end = range_end
51
+
52
+ @override
53
+ def sample_int(self, seed: int | None = None) -> int:
54
+ """
55
+ Get uniform random int from range [self.beg_range, self.end_range]
56
+
57
+ Parameters
58
+ ----------
59
+ seed : int | None, optional
60
+ set the new seed, if None does nothing
61
+
62
+ Returns
63
+ -------
64
+ int
65
+ random integer from range [self.beg_range, self.end_range]
66
+ """
67
+ self.set_seed(seed)
68
+ return self._pyrng.randint(self._range_beg, self._range_end)
69
+
70
+ @override
71
+ def sample_vector(self, n: int, seed: int | None = None) -> Vector:
72
+ """
73
+ _summary_
74
+
75
+ Parameters
76
+ ----------
77
+ n : int
78
+ _description_
79
+ seed : int | None, optional
80
+ _description_, by default None
81
+
82
+ Returns
83
+ -------
84
+ Vector
85
+ _description_
86
+ """
87
+ self.set_seed(seed)
88
+ return as_integer([self.sample_int() for _ in range(n)])
89
+
90
+ @override
91
+ def sample_matrix(self, rows: int, cols: int | None = None, seed: int | None = None) -> Matrix:
92
+ """
93
+ _summary_
94
+
95
+ Parameters
96
+ ----------
97
+ rows : int
98
+ _description_
99
+ cols : int | None, optional
100
+ _description_, by default None
101
+ seed : int | None, optional
102
+ _description_, by default None
103
+
104
+ Returns
105
+ -------
106
+ Matrix
107
+ _description_
108
+ """
109
+ self.set_seed(seed)
110
+ if cols is None:
111
+ cols = rows
112
+ return as_integer([[self.sample_int() for _ in range(cols)] for _ in range(rows)])
113
+
114
+ @override
115
+ def set_params(self, range_beg: int | None = None, range_end: int | None = None) -> None: # type: ignore
116
+ """
117
+ _summary_
118
+
119
+ Parameters
120
+ ----------
121
+ range_beg : int | None, optional
122
+ _description_, by default None
123
+ range_end : int | None, optional
124
+ _description_, by default None
125
+ """
126
+ if range_beg is None:
127
+ range_beg = self._range_beg
128
+ if range_end is None:
129
+ range_end = self._range_end
130
+
131
+ self._range_beg = range_beg
132
+ self._range_end = range_end
133
+
134
+ @override
135
+ def get_params(self) -> dict[str, int]:
136
+ """
137
+ _summary_
138
+
139
+ Returns
140
+ -------
141
+ dict[str, int]
142
+ _description_
143
+ """
144
+ return {"range_beg": self._range_beg, "range_end": self._range_end}
145
+
146
+
147
+ class DiscreteGaussian(Distribution):
148
+ def __init__(self, sigma: float, center: int | float = 0, tail_cut: float = 6.0, seed: int | None = None):
149
+ """
150
+ _summary_
151
+
152
+ Parameters
153
+ ----------
154
+ sigma : float
155
+ _description_
156
+ center : int | float, optional
157
+ _description_, by default 0
158
+ tail_cut : float, optional
159
+ _description_, by default 6.0
160
+ seed : int | None, optional
161
+ _description_, by default None
162
+ """
163
+ super().__init__(seed)
164
+ self.center = center
165
+ self.sigma = sigma
166
+ self.tail_cut = tail_cut
167
+
168
+ self._table: dict[int, float] = {}
169
+ self._recompute_table()
170
+
171
+ def _recompute_table(self) -> None:
172
+ self.bound = int(math.ceil(self.tail_cut * self.sigma))
173
+ self._table: dict[int, float] = {}
174
+ for x in range(-self.bound, self.bound + 1):
175
+ self._table[x] = math.exp(-(x**2) / (2 * self.sigma**2))
176
+
177
+ @override
178
+ def sample_int(self, seed: int | None = None) -> int:
179
+ """
180
+ _summary_
181
+
182
+ Parameters
183
+ ----------
184
+ seed : int | None, optional
185
+ _description_, by default None
186
+
187
+ Returns
188
+ -------
189
+ int
190
+ _description_
191
+ """
192
+ self.set_seed(seed)
193
+
194
+ if isinstance(self.center, int):
195
+ return self._sample_centered_fast() + self.center
196
+
197
+ return self._sample_dynamic_rejection(self.center)
198
+
199
+ @override
200
+ def sample_vector(self, n: int, seed: int | None = None) -> Vector:
201
+ """
202
+ _summary_
203
+
204
+ Parameters
205
+ ----------
206
+ n : int
207
+ _description_
208
+ seed : int | None, optional
209
+ _description_, by default None
210
+
211
+ Returns
212
+ -------
213
+ Vector
214
+ _description_
215
+ """
216
+ self.set_seed(seed)
217
+ return as_integer([self.sample_int() for _ in range(n)])
218
+
219
+ @override
220
+ def sample_matrix(self, rows: int, cols: int | None = None, seed: int | None = None) -> Matrix:
221
+ """
222
+ _summary_
223
+
224
+ Parameters
225
+ ----------
226
+ rows : int
227
+ _description_
228
+ cols : int | None, optional
229
+ _description_, by default None
230
+ seed : int | None, optional
231
+ _description_, by default None
232
+
233
+ Returns
234
+ -------
235
+ Matrix
236
+ _description_
237
+ """
238
+ self.set_seed(seed)
239
+ if cols is None:
240
+ cols = rows
241
+ return as_integer([[self.sample_int() for _ in range(cols)] for _ in range(rows)])
242
+
243
+ @override
244
+ def set_params(self, sigma: float | None = None, center: float | int | None = None, tail_cut: float | None = None) -> None: # type: ignore
245
+ """
246
+ _summary_
247
+
248
+ Parameters
249
+ ----------
250
+ sigma : float | None, optional
251
+ _description_, by default None
252
+ center : float | int | None, optional
253
+ _description_, by default None
254
+ tail_cut : float | None, optional
255
+ _description_, by default None
256
+ """
257
+ if sigma is None:
258
+ sigma = self.sigma
259
+ if center is None:
260
+ center = self.center
261
+ if tail_cut is None:
262
+ tail_cut = self.tail_cut
263
+
264
+ self.sigma = sigma
265
+ self.tail_cut = tail_cut
266
+ self.center = center
267
+ self._recompute_table()
268
+
269
+ def get_params(self) -> dict[str, float]:
270
+ """
271
+ _summary_
272
+
273
+ Returns
274
+ -------
275
+ dict[str, float]
276
+ _description_
277
+ """
278
+ return {"sigma": self.sigma, "center": self.center, "tail_cut": self.tail_cut, "bound": self.bound}
279
+
280
+ def _sample_centered_fast(self) -> int:
281
+ max_iters = 1000
282
+ for _ in range(max_iters):
283
+ x = random.randint(-self.bound, self.bound)
284
+ prob = self._table.get(x, 0.0)
285
+ if random.random() < prob:
286
+ return x
287
+
288
+ raise RuntimeError("Failed to generate sample")
289
+
290
+ def _sample_dynamic_rejection(self, c: float) -> int:
291
+ start = int(math.floor(c - self.bound))
292
+ end = int(math.ceil(c + self.bound))
293
+
294
+ max_iters = 1000
295
+ for _ in range(max_iters):
296
+ x = random.randint(start, end)
297
+ dist_sq = (x - c) ** 2
298
+ prob = math.exp(-dist_sq / (2 * self.sigma**2))
299
+
300
+ if random.random() < prob:
301
+ return x
302
+
303
+ raise RuntimeError("Failed to generate sample")
@@ -0,0 +1,53 @@
1
+ import random
2
+
3
+ import numpy as np
4
+
5
+ from .._utils import as_integer, zeros_mat
6
+ from ..typing import SquareMatrix
7
+
8
+
9
+ def _gen_unimodular(n: int, rounds: int, rng: random.Random) -> SquareMatrix:
10
+ U = as_integer(np.eye(n))
11
+ for _ in range(rounds):
12
+ i, j = rng.sample(range(n), 2)
13
+ coeff = rng.sample([-1, 1], 1)
14
+
15
+ U[i] += coeff * U[j]
16
+
17
+ return U
18
+
19
+
20
+ def randlattice(n: int, det_upper_bound: int | None = None, seed: int | None = None) -> SquareMatrix:
21
+ """
22
+ Generates lattice basis by, first generating random square matrix in Hermite normal form and then by transforming it using random unimodular matrix.
23
+
24
+ Parameters
25
+ ----------
26
+ n : int
27
+ lattice's rank
28
+ det_upper_bound : int | None, optional
29
+ upper bound of lattice volume, by default 2 ** n
30
+ seed : int | None, optional
31
+ seed for random number generator
32
+
33
+ Returns
34
+ -------
35
+ SquareMatrix
36
+ n x n matrix representing lattice basis
37
+ """
38
+ det_ub: int = 2**n if det_upper_bound is None else det_upper_bound
39
+
40
+ rng = random.Random(seed)
41
+ diagonals = [rng.randint(1, det_ub) for _ in range(n)]
42
+
43
+ H = zeros_mat(n)
44
+ for i in range(n):
45
+ H[i, i] = diagonals[i]
46
+ modulus = H[i, i]
47
+ for j in range(i + 1, n):
48
+ H[i, j] = rng.randint(0, modulus - 1)
49
+
50
+ U = _gen_unimodular(n, n * 5, rng)
51
+
52
+ basis = U @ H
53
+ return basis
@@ -0,0 +1,109 @@
1
+ import numpy as np
2
+
3
+ from ..integer._modring import cmodl, mod
4
+ from ..typing import Matrix, Vector
5
+ from ._distribution import DiscreteGaussian, Uniform
6
+
7
+
8
+ class LWE:
9
+ def __init__(self, n: int, q: int, sigma: float, secret_distribution: str, seed: int):
10
+ """
11
+ Creates LWE sampler with DiscreteGuassianDistribution centered at 0 as noise sampler
12
+
13
+ Parameters
14
+ ----------
15
+ n : int
16
+ length of secret vector
17
+ q : int
18
+ modulus
19
+ sigma : float
20
+ sigma value for DiscreteGaussianDistribution
21
+ seed : int
22
+ seed for random number generator
23
+ """
24
+ self.n = n
25
+ self.q = q
26
+ self.U = Uniform(0, q - 1, seed=seed)
27
+ self.D = DiscreteGaussian(sigma, seed=seed)
28
+
29
+ secret = self.U.sample_vector(n)
30
+ if secret_distribution == "uniform":
31
+ self._secret = secret
32
+ elif secret_distribution == "binary":
33
+ self._secret = mod(secret, 2)
34
+ elif secret_distribution == "ternary":
35
+ self._secret = cmodl(secret, 3)
36
+ else:
37
+ raise ValueError(f"Unknown distribution {secret_distribution}, expected uniform|binary|ternary")
38
+
39
+ @property
40
+ def secret(self) -> Vector:
41
+ """
42
+ Retrieve underlying secret
43
+
44
+ Returns
45
+ -------
46
+ Vector
47
+ s: n-vector
48
+ """
49
+ return self._secret
50
+
51
+ def set_secret(self, secret: Vector) -> None:
52
+ """
53
+ Set the underlying secret
54
+
55
+ Parameters
56
+ ----------
57
+ secret : Vector
58
+ secret vector to set
59
+
60
+ Raises
61
+ ------
62
+ ValueError
63
+ when lenght of the provided vector is not correct with the parameter of the LWE sampler
64
+ """
65
+ if secret.shape[0] != self.n:
66
+ raise ValueError(f"expected {self.n} dimension of secret, got {secret.shape[0]}")
67
+
68
+ self._secret = secret
69
+
70
+ def sample_matrix(self, m: int) -> tuple[Matrix, Vector]:
71
+ """
72
+ Generates a full matrix system (A, b) with 'm' samples.
73
+
74
+ Parameters
75
+ ----------
76
+ m : int
77
+ how many samples should the resulting matrix have
78
+
79
+ Returns
80
+ -------
81
+ tuple[Matrix, Vector]:
82
+
83
+ A (Matrix): m x n matrix (Uniform mod q)
84
+ b (Vector): m-vector (As + e mod q)
85
+ """
86
+ A = self.U.sample_matrix(m, self.n)
87
+ e = self.D.sample_vector(m)
88
+
89
+ b = mod(A @ self.secret + e, self.q)
90
+
91
+ return A, b
92
+
93
+ def next_sample(self) -> tuple[Vector, int]:
94
+ """
95
+ Generates a single sample pair (a, b).
96
+
97
+ Returns
98
+ -------
99
+ tuple[Vector, int]
100
+ a (Vector): n-vector (Uniform mod q)
101
+ b (int): as + e mod q
102
+ """
103
+ a = self.U.sample_vector(self.n)
104
+ e = self.D.sample_int()
105
+
106
+ _as: int = np.dot(a, self.secret)
107
+ b = mod(_as + e, self.q)
108
+
109
+ return a, b
@@ -0,0 +1,41 @@
1
+ from typing import overload
2
+
3
+ from ..typing import Matrix, Vector
4
+
5
+
6
+ class LWR:
7
+ def __init__(self):
8
+ """_summary_
9
+
10
+ Raises
11
+ ------
12
+ NotImplementedError
13
+ _description_
14
+ """
15
+ raise NotImplementedError()
16
+
17
+ @overload
18
+ def __call__(self, n: int) -> Matrix: ...
19
+
20
+ @overload
21
+ def __call__(self, n: None) -> Vector: ...
22
+
23
+ def __call__(self, n: int | None = None) -> Matrix | Vector:
24
+ """_summary_
25
+
26
+ Parameters
27
+ ----------
28
+ n : int | None, optional
29
+ _description_, by default None
30
+
31
+ Returns
32
+ -------
33
+ Matrix | Vector
34
+ _description_
35
+
36
+ Raises
37
+ ------
38
+ NotImplementedError
39
+ _description_
40
+ """
41
+ raise NotImplementedError()
@@ -0,0 +1,53 @@
1
+ import math
2
+ import random
3
+
4
+ from ..integer._primality import is_prime
5
+
6
+
7
+ def _randprime(a: int, b: int, seed: int | None = None) -> int:
8
+ def ilog(n: int, base: float = math.e) -> int:
9
+ return int((n.bit_length() - 1) / math.log2(base))
10
+
11
+ try:
12
+ approx_number_of_primes_to_a = 0 if a == 0 else a // ilog(a)
13
+ approx_number_of_primes_to_b = 0 if b == 0 else b // ilog(b)
14
+ approx_number_of_primes = approx_number_of_primes_to_b - approx_number_of_primes_to_a
15
+ prime_proba = approx_number_of_primes / (b - a)
16
+ number_of_samples = int(math.log(0.001) / math.log(1 - prime_proba)) + 1
17
+ except ZeroDivisionError:
18
+ number_of_samples = b - a
19
+
20
+ if b - a < 1000:
21
+ number_of_samples = b - a
22
+
23
+ random.seed(seed)
24
+ for i in range(number_of_samples):
25
+ prime_candidate = random.randint(a, b)
26
+ if is_prime(prime_candidate):
27
+ return prime_candidate
28
+ if is_prime(a + i):
29
+ return a + i
30
+
31
+ raise ValueError(f"Couldn't find a prime number in interval [{a}, {b})")
32
+
33
+
34
+ def randprime(kbits: int, seed: int | None = None) -> int:
35
+ """
36
+ Generates random prime number from range [2 ** (kbits - 1); 2 ** (kbist)].
37
+ Uses Miller-Rabin primality test.
38
+
39
+ Parameters
40
+ ----------
41
+ kbits : int
42
+ number of bits the prime number should have
43
+ seed : int | None, optional
44
+ seed for random number generator, by default None
45
+
46
+ Returns
47
+ -------
48
+ int
49
+ prime number
50
+ """
51
+ a = 2 ** (kbits - 1)
52
+ b = 2**kbits
53
+ return _randprime(a, b, seed=seed)
@@ -0,0 +1,3 @@
1
+ from ._distribution import DiscreteGaussian, Uniform
2
+
3
+ __all__ = ["Uniform", "DiscreteGaussian"]
pqlattice/settings.py ADDED
@@ -0,0 +1,66 @@
1
+ import contextlib
2
+ from collections.abc import Generator
3
+ from typing import Literal
4
+
5
+ from ._backends._fast import FastBackend
6
+ from ._backends._native import NativeBackend
7
+ from ._backends._protocol import BackendInterface
8
+
9
+ BackendName = Literal["native", "fast"]
10
+
11
+
12
+ class _Settings:
13
+ def __init__(self) -> None:
14
+ self._active_backend_name: BackendName = "native"
15
+ self._native_backend = NativeBackend()
16
+ self._fast_backend: FastBackend | None = None
17
+
18
+ @property
19
+ def backend_name(self) -> BackendName:
20
+ return self._active_backend_name
21
+
22
+ @property
23
+ def backend(self) -> BackendInterface:
24
+ if self.backend_name == "native":
25
+ return self._native_backend
26
+ elif self.backend_name == "fast":
27
+ if self._fast_backend is None:
28
+ self._fast_backend = FastBackend()
29
+ return self._fast_backend
30
+
31
+ raise RuntimeError(f"No backend {self.backend_name}")
32
+
33
+ def set_backend(self, name: BackendName) -> None:
34
+ self._active_backend_name = name
35
+
36
+
37
+ _config = _Settings()
38
+
39
+
40
+ def get_backend_name() -> BackendName:
41
+ return _config.backend_name
42
+
43
+
44
+ def get_backend() -> BackendInterface:
45
+ return _config.backend
46
+
47
+
48
+ def set_backend(name: BackendName) -> None:
49
+ _config.set_backend(name)
50
+
51
+
52
+ @contextlib.contextmanager
53
+ def backend(name: BackendName) -> Generator[None, None, None]:
54
+ """
55
+ Context manager to temporarily switch the backend.
56
+
57
+ Usage:
58
+ with pq.settings.backend("fpylll"):
59
+ pq.lattice.lll(B)
60
+ """
61
+ previous = _config.backend_name
62
+ try:
63
+ set_backend(name)
64
+ yield
65
+ finally:
66
+ set_backend(previous)
@@ -0,0 +1,4 @@
1
+ from ._types import Array, Matrix, SquareMatrix, Vector, is_integer, is_rational
2
+ from ._types_validator import is_Matrix, is_SquareMatrix, is_Vector, validate_aliases
3
+
4
+ __all__ = ["Array", "Matrix", "SquareMatrix", "Vector", "is_integer", "is_rational", "validate_aliases", "is_Vector", "is_Matrix", "is_SquareMatrix"]
@@ -0,0 +1,18 @@
1
+ from fractions import Fraction
2
+ from typing import Any
3
+
4
+ from numpy.typing import NDArray
5
+
6
+ type Array = NDArray[Any]
7
+
8
+ type Vector = NDArray[Any]
9
+ type Matrix = NDArray[Any]
10
+ type SquareMatrix = NDArray[Any]
11
+
12
+
13
+ def is_rational(a: Array) -> bool:
14
+ return isinstance(a.flat[0], Fraction)
15
+
16
+
17
+ def is_integer(a: Array) -> bool:
18
+ return isinstance(a.flat[0], int)