kryptools 0.1__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.
kryptools/lat.py ADDED
@@ -0,0 +1,168 @@
1
+ """
2
+ Lattice tools
3
+ """
4
+
5
+ from math import prod
6
+ from fractions import Fraction
7
+ from .la import Matrix, zeros, eye
8
+
9
+ def babai_round_cvp(x: Matrix, U: Matrix) -> Matrix:
10
+ "Babai's rounding algorithm for solving the CVP."
11
+ s = U.inv() * x
12
+ k = s.map(round)
13
+ return U * k
14
+
15
+ def hermite_nf(M: Matrix) -> Matrix:
16
+ "Compute the Hermite normal form of a matrix M."
17
+ n, m = M.cols, M.rows
18
+ H = M[:, :]
19
+ j = n - 1
20
+ for i in reversed(range(m)):
21
+ j0 = j
22
+ minimum = abs(H[i, j]) # search for the pivot in the present row
23
+ for jj in range(j):
24
+ tmp = abs(H[i, jj])
25
+ if tmp > 0 and (tmp < minimum or minimum == 0):
26
+ minimum = tmp
27
+ j0 = jj
28
+ if minimum == 0:
29
+ continue # all entries are zero
30
+ if j0 < j:
31
+ H[:, j], H[:, j0] = (
32
+ H[:, j0],
33
+ H[:, j],
34
+ ) # swap columns, to move the pivot in place
35
+ if H[i, j] < 0:
36
+ H[:, j] *= -1 # make the pivot positive
37
+ jj = j - 1
38
+ while jj >= 0: # make the row entries left to the pivot zero
39
+ tmp = H[i, jj] // H[i, j]
40
+ H[:, jj] -= tmp * H[:, j]
41
+ if H[i, jj]:
42
+ H[:, j], H[:, jj] = H[:, jj], H[:, j] # swap columns
43
+ else:
44
+ jj -= 1
45
+ for jj in range(j + 1, n): # reduce the row entries right to the pivot
46
+ tmp = H[i, jj] // H[i, j]
47
+ H[:, jj] -= tmp * H[:, j]
48
+ #print(H)
49
+ j -= 1
50
+ while H.cols> 1 and all(not H[i, 0] for i in range(m)): # remove zero columns
51
+ H = H[:,1:]
52
+ return H
53
+
54
+ def norm2(v: Matrix) -> float:
55
+ "Square of the Euclidean norm of a vector v."
56
+ return sum(map(lambda x: x * x, v))
57
+
58
+ def gram_schmidt(U: Matrix) -> (Matrix, Matrix):
59
+ "Compute the Gram-Schmidt orthogonalization of the column vectors of a matrix M."
60
+ M = eye(U.cols, U.rows)
61
+ Us = U[:, :]
62
+ for j in range(1, U.rows):
63
+ tmp = U[:, j]
64
+ for i in range(j):
65
+ M[i, j] = U[:, j].dot(Us[:, i]) / norm2(Us[:, i])
66
+ tmp -= M[i, j] * Us[:, i]
67
+ Us[:, j] = tmp
68
+ return Us, M
69
+
70
+ def gram_det(U: Matrix) -> float:
71
+ Us = gram_schmidt(U)[0]
72
+ return prod([Us[:, i].norm() for i in range(U.rows)])
73
+
74
+ def hadamard_ratio(M: Matrix) -> float:
75
+ m = M.rows
76
+ return (gram_det(M) / prod([M[:, i].norm() for i in range(m)])) ** (1 / m)
77
+
78
+ def babai_plane_cvp(x: Matrix, U: Matrix) -> Matrix:
79
+ "Babai's closest plane algorithm for solving the CVP."
80
+ Us = gram_schmidt(U)[0]
81
+ y = x
82
+ for k in range(U.cols - 1, -1, -1):
83
+ y = y - round(y.dot(Us[:, k]) / norm2(Us[:, k])) * U[:, k]
84
+ return (x - y).applyfunc(round)
85
+
86
+ def lll(V: Matrix, delta: float = 0.75, sort: bool = True) -> Matrix:
87
+ "lll algorithm for lattice reduction"
88
+
89
+ assert 0 < delta <= 1, f"LLL reqires 0 < delta={delta} <= 1"
90
+ j = 1
91
+ U = V[:, :]
92
+ Us = U[:, :]
93
+ Us.map(Fraction)
94
+ M = zeros(U.cols, U.rows)
95
+ M.map(Fraction)
96
+ M[0, 0] = norm2(Us[:, 0]) # we store the squared norms on the diagonal
97
+ for l in range(1, U.rows): # Gram-Schmidt decomposition
98
+ tmp = U[:, l]
99
+ for i in range(l):
100
+ M[i, l] = U[:, l].dot(Us[:, i]) / M[i, i]
101
+ tmp -= M[i, l] * Us[:, i]
102
+ Us[:, l] = tmp
103
+ M[l, l] = norm2(Us[:, l])
104
+
105
+ while j < U.rows:
106
+ for i in range(j - 1, -1, -1): # reduce the weights of the basis vectors
107
+ r = round(M[i, j])
108
+ if r:
109
+ U[:, j] -= r * U[:, i]
110
+ for k in range(j):
111
+ if k == i:
112
+ M[k, j] -= r
113
+ else:
114
+ M[k, j] -= r * M[k, i]
115
+
116
+ newM11 = M[j, j] + M[j - 1, j] ** 2 * M[j - 1, j - 1]
117
+ if (delta * M[j - 1, j - 1] <= newM11): # Lovasz condition
118
+ j += 1
119
+ continue # nothing to be done
120
+ # else swap vectors
121
+ U[:, j], U[:, j - 1] = U[:, j - 1], U[:, j]
122
+ # update the Gram-Schmidt decomposition
123
+ oldM11 = M[j - 1, j - 1]
124
+ oldM10 = M[j - 1, j]
125
+ oldM00 = M[j, j]
126
+ oldUs = Us[:, j - 1]
127
+ Us[:, j - 1] = Us[:, j] + M[j - 1, j] * Us[:, j - 1]
128
+ M[j - 1, j - 1] = newM11
129
+ M[j - 1, j] *= oldM11 / M[j - 1, j - 1]
130
+ Us[:, j] = oldUs - M[j - 1, j] * Us[:, j - 1]
131
+ M[j, j] = oldM11 - M[j - 1, j] ** 2 * M[j - 1, j - 1]
132
+ for l in range(j - 1):
133
+ M[l, j], M[l, j - 1] = M[l, j - 1], M[l, j]
134
+ tmp1 = oldM00 / M[j - 1, j - 1]
135
+ tmp2 = oldM10 * oldM11 / M[j - 1, j - 1]
136
+ for l in range(j + 1, U.rows):
137
+ M[j - 1, l], M[j, l] = (
138
+ tmp1 * M[j, l] + tmp2 * M[j - 1, l],
139
+ M[j - 1, l] - oldM10 * M[j, l],
140
+ )
141
+ j = max(j - 1, 1) # redo the last step
142
+
143
+ if sort: # sort the vectors according to their norm
144
+ tmp = [U[:, j] for j in range(U.rows)]
145
+ tmp.sort(key=norm2)
146
+ for j in range(U.rows):
147
+ U[:, j] = tmp[j]
148
+ return U
149
+
150
+ from random import choice, sample
151
+
152
+ def random_unimodular_matrix(n: int, iterations: int = 50, max_val: int = 9) -> Matrix:
153
+ "Create a random unimodular matrix of dimension n."
154
+ W = Matrix.zeros(n, n)
155
+ for i in range(n):
156
+ for j in range(i, n):
157
+ W[i, j] = choice([-1, 1])
158
+ W = W[sample(range(n), n), sample(range(n), n)]
159
+ for _ in range(iterations):
160
+ i, j = sample(range(n), 2)
161
+ tmp = W[i, :] + choice([-1, 1]) * W[j, :]
162
+ if max([abs(x) for x in tmp]) <= max_val:
163
+ W[i, :] = tmp
164
+ i, j = sample(range(n), 2)
165
+ tmp = W[:, i] + choice([-1, 1]) * W[:, j]
166
+ if max([abs(x) for x in tmp]) <= max_val:
167
+ W[:, i] = tmp
168
+ return W
kryptools/nt.py ADDED
@@ -0,0 +1,271 @@
1
+ """
2
+ Number theory tools:
3
+ lcm(a, b) least common mutiple of a and b
4
+ egcd(a,b) extended Euclidean agorithm
5
+ crt([a1, a2, ...],[m1, m2, ...]) Chinese Remainder Theorem
6
+ cf(Fraction(m,n)) continued fraction expansions
7
+ convergents() convergents of a continued fraction
8
+ sqrt_mod(n, p) square root of n modulo a prime p
9
+ order(a, n) oder of a in the multiplicative group Z_n^*
10
+ """
11
+ from math import gcd, prod
12
+ from fractions import Fraction
13
+ from .factor import factorint
14
+
15
+ # Euclid and friends
16
+
17
+ def lcm(a: int, b: int) -> int:
18
+ """Compute the least common multiple of a and b."""
19
+ if b == 0:
20
+ return 0
21
+ if bool(a > 0) != bool(b > 0):
22
+ a = -a
23
+ return (a // gcd(a, b)) * b
24
+
25
+
26
+ def egcd(a: int, b: int) -> (int, int, int):
27
+ """Perform the extended Euclidean agorithm. Returns gcd, x, y such that a x + b y = gcd."""
28
+ r0, r1 = a, b
29
+ x0, x1, y0, y1 = 1, 0, 0, 1
30
+ while r1 != 0:
31
+ q, r = divmod(r0, r1)
32
+ r0, r1 = r1, r
33
+ x0, x1 = x1, x0 - q * x1
34
+ y0, y1 = y1, y0 - q * y1
35
+ return r0, x0, y0
36
+
37
+
38
+ # Chinese remainder theorem
39
+
40
+ def crt(a: list, m: list) -> int:
41
+ """Solve given linear congruences x[j] % m[j] == a[j] using the Chinese Remainder Theorem."""
42
+ l = len(a)
43
+ assert len(m) == l, "The lists of numbers and modules must have equal length."
44
+ M = prod(m)
45
+ Mi = [M // m[i] for i in range(l)]
46
+ MNi = [Mi[i] * pow(Mi[i], -1, m[i]) % M for i in range(l)]
47
+ return sum([a[i] * MNi[i] % M for i in range(l)]) % M
48
+
49
+ # Continued fractions
50
+
51
+ # class Fraction:
52
+ # "Rationl number"
53
+ #
54
+ # def __init__(self, numerator: int, denominator: int = 1):
55
+ # assert denominator, "Denominator must be nozero."
56
+ # tmp = gcd(denominator, numerator)
57
+ # if tmp != 1:
58
+ # denominator //= tmp
59
+ # numerator //= tmp
60
+ # self.numerator = numerator
61
+ # self.denominator = denominator
62
+ #
63
+ # def __repr__(self):
64
+ # if self.denominator == 1:
65
+ # return str(self.numerator)
66
+ # else:
67
+ # return str(self.numerator) + "/" + str(self.denominator)
68
+ #
69
+ # def __eq__(self, other):
70
+ # if not isinstance(other, self.__class__):
71
+ # return False
72
+ # return self.denominator == other.denominator and self.numerator == other.numerator
73
+ #
74
+ # def __bool__(self):
75
+ # return self.numerator != 0
76
+ #
77
+ # def __add__(self, other: "Fraction") -> "Fraction":
78
+ # if isinstance(other, self.__class__):
79
+ # return Rational(self.numerator * other.denominator + other.numerator * self.denominator, self.denominator * other.denominator)
80
+ # if isinstance(other, int):
81
+ # return Rational(self.numerator + other * self.denominator, self.denominator)
82
+ # raise ValueError(f"Cannot add {self} and {other}.")
83
+ #
84
+ # def __neg__(self) -> "Fraction":
85
+ # return Rational(- self.numerator, self.denominator)
86
+ #
87
+ # def __sub__(self, other: "Fraction") -> "Fraction":
88
+ # if isinstance(other, self.__class__):
89
+ # return Rational(self.numerator * other.denominator - other.numerator * self.denominator, self.denominator * other.denominator)
90
+ # if isinstance(other, int):
91
+ # return Rational(self.numerator - other * self.denominator, self.denominator)
92
+ # raise ValueError(f"Cannot subtract {self} and {other}.")
93
+ #
94
+ # def __mul__(self, other: "Fraction") -> "Fraction":
95
+ # if isinstance(other, self.__class__):
96
+ # return Rational(self.numerator * other.numerator, self.denominator * other.denominator)
97
+ # if isinstance(other, int):
98
+ # return Rational(self.numerator * other, self.denominator)
99
+ # raise ValueError(f"Cannot multiply {self} and {other}.")
100
+ #
101
+ # def __rmul__(self, other: int) -> "Fraction":
102
+ # return Rational(self.denominator * other, self.numerator)
103
+ #
104
+ # def __truediv__(self, other: "Fraction") -> "Fraction":
105
+ # if isinstance(other, self.__class__):
106
+ # return Rational(self.numerator * other.denominator, self.denominator * other.numerator)
107
+ # if isinstance(other, int):
108
+ # return Rational(self.numerator, self.denominator * other)
109
+ # raise ValueError(f"Cannot divide {self} and {other}.")
110
+ #
111
+ # def __pow__(self, scalar: int) -> "Fraction":
112
+ # return Rational(self.numerator**scalar, self.denominator**scalar)
113
+
114
+
115
+ def fraction_repr(self):
116
+ if self.denominator == 1:
117
+ return str(self.numerator)
118
+ else:
119
+ return str(self.numerator) + "/" + str(self.denominator)
120
+
121
+ Fraction.__repr__ = fraction_repr
122
+
123
+
124
+ def cf(x: Fraction) -> list[int]:
125
+ "Compute the continued fraction expansion of the rational number m/n."
126
+ res = []
127
+ m = x.numerator
128
+ n = x.denominator
129
+ while True:
130
+ d, m = divmod(m, n)
131
+ res.append(d)
132
+ if not m:
133
+ return res
134
+ m, n = n, m
135
+
136
+
137
+ def convergents(cont_frac: list[int]) -> float:
138
+ "Compute the convergents of a continued fraction expansion."
139
+ res = []
140
+
141
+ def recursion(x: int, m: int, mm: int) -> (int, int):
142
+ return x * m + mm, m
143
+
144
+ m, mm = 1, 0
145
+ n, nn = 0, 1
146
+ for x in cont_frac:
147
+ m, mm = recursion(x, m, mm)
148
+ n, nn = recursion(x, n, nn)
149
+ res.append(Fraction(m, n))
150
+ return res
151
+
152
+
153
+ # Legendre symbol
154
+
155
+ def legendre_symbol(a: int, p: int) -> int:
156
+ """Compute the Legendre symbol of a with respect to the prime p."""
157
+ a = a % p
158
+ if a == 0:
159
+ return 0
160
+ if pow(a, (p - 1) // 2, p) == 1:
161
+ return 1
162
+ return -1
163
+
164
+ def jacobi_symbol(a: int, n: int) -> int:
165
+ """Compute the Jacobi symbol of a with respect to the integer n."""
166
+ # Crandall/Pomerance Algorithm 2.3.5
167
+ a = a % n
168
+ t = 1
169
+ while a:
170
+ while a % 2 == 0:
171
+ a //= 2
172
+ tmp = n % 8
173
+ if tmp == 3 or tmp == 5:
174
+ t *= -1
175
+ a, n = n, a
176
+ if a % 4 == 3 and n % 4 == 3:
177
+ t *= -1
178
+ a %= n
179
+ if n == 1:
180
+ return t
181
+ return 0
182
+
183
+ def sqrt_mod(a: int, p: int) -> list:
184
+ "Compute a square root of a modulo p unsing Cipolla's algorithm."
185
+ a %= p
186
+ if a == 0 or a == 1:
187
+ return a
188
+ if pow(a, (p - 1) // 2, p) != 1: # Legendre symbol must be equal one
189
+ return None
190
+ if p % 4 == 3: # easy case
191
+ return pow(a, (p + 1) // 4, p)
192
+ # Cipolla's algorithm
193
+ for c in range(1, p): # find a field extension
194
+ tmp = (c * c - a) % p
195
+ if tmp == 0:
196
+ return c
197
+ if pow(tmp, (p - 1) // 2, p) == p - 1:
198
+ break
199
+ r = (c * c - a) % p
200
+ i = (p + 1) // 2
201
+ x1, x2 = c, 1 # compute x^i in Z_p(sqrt(r))
202
+ y1, y2 = 1, 0
203
+ while i > 0:
204
+ if i & 1: # if i is odd, multiply with x
205
+ y1, y2 = (x1 * y1 + x2 * y2 * r) % p, (x1 * y2 + x2 * y1) % p
206
+ x1, x2 = (pow(x1, 2, p) + pow(x2, 2, p) * r) % p, 2 * (x1 * x2) % p # now square
207
+ i = i >> 1 # i= i/2
208
+ return y1
209
+
210
+ # Euler phi and Carmichael function
211
+
212
+ from math import prod
213
+
214
+
215
+ def euler_phi(n: int) -> int:
216
+ """Euler's phi function of n."""
217
+ k = factorint(n)
218
+ return prod([(p - 1) * p ** (k[p] - 1) for p in k])
219
+
220
+
221
+ def carmichael_lambda(n: int) -> int:
222
+ """Carmichael's lambda function of n."""
223
+ k = factorint(n)
224
+ lam_all = [] # values corresponding to the prime factors
225
+ for p in k:
226
+ lam = (p - 1) * p ** (k[p] - 1)
227
+ if p == 2 and k[p] > 2:
228
+ lam = lam // 2
229
+ lam_all += [lam]
230
+ lam = lam_all[0] # now take the least common multiple of all values
231
+ for l in lam_all[1:]:
232
+ lam = lcm(lam, l)
233
+ return lam
234
+
235
+ # Order in Z_p^*
236
+
237
+ def order(a: int, n: int, factor=False) -> int:
238
+ """Compute the order of a in the group Z_n^*."""
239
+ a %= n
240
+ assert a != 0 and gcd(a, n) == 1, f"{a} and {n} are not coprime!"
241
+ factors = dict() # We compute euler_phi(n) and its factorization in one pass
242
+ for p, k in factorint(n).items(): # first factorize n
243
+ for pm, km in factorint(p - 1).items(): # factor p-1 and add the factors
244
+ if pm in factors:
245
+ factors[pm] += km
246
+ else:
247
+ factors[pm] = km
248
+ if k > 1: # if the multiplicity of of p is >1, then we need to add p**(k-1)
249
+ if p in factors:
250
+ factors[p] += k - 1
251
+ else:
252
+ factors[p] = k - 1
253
+ order = 1 # compute the group order euler_phi(n) as our current guess
254
+ for p, k in factors.items():
255
+ order *= p**k
256
+ if factor: # we compute the factorization of the order along the way
257
+ factors_order = {} # factorization of the order
258
+ for p, k in factors.items():
259
+ i = 0
260
+ for _ in range(k):
261
+ order_try = order // p
262
+ if pow(a, order_try, n) == 1:
263
+ order = order_try
264
+ i += 1
265
+ else:
266
+ break
267
+ if factor and i < k:
268
+ factors_order[p] = k - i
269
+ if factor:
270
+ return order, factors_order
271
+ return order
kryptools/poly.py ADDED
@@ -0,0 +1,232 @@
1
+ """
2
+ Polynomials
3
+ """
4
+
5
+ class Poly:
6
+ """
7
+ Represents a polynomial as a list of coefficients.
8
+
9
+ Example:
10
+
11
+ To define a polynomial as alist of coefficients use
12
+ >>> Poly([1, 2, 3])
13
+ 3 x^2 + 2 x + 1
14
+ """
15
+
16
+ def __init__(self, coeff: list, ring = None, modulus: list = None):
17
+ for i in range(len(coeff) - 1, 0, -1):
18
+ if coeff[i]:
19
+ break
20
+ coeff.pop(i)
21
+ self.coeff = coeff
22
+ self.modulus = modulus
23
+ if ring:
24
+ self.map(ring)
25
+ if modulus:
26
+ self.mod(modulus)
27
+
28
+ def __getitem__(self, item):
29
+ return self.coeff[item]
30
+
31
+ def __repr__(self):
32
+ def prx(i: int):
33
+ if i == 0:
34
+ return ""
35
+ if i == 1:
36
+ return "x"
37
+ return "x^" + str(i)
38
+
39
+ if len(self.coeff) == 1:
40
+ return str(int(self.coeff[0]))
41
+ plus = ""
42
+ tmp = ""
43
+ for i in reversed(range(len(self.coeff))):
44
+ s = self.coeff[i]
45
+ if not s:
46
+ continue
47
+ if not s - 1 and i != 0:
48
+ tmp += plus + prx(i)
49
+ plus = " + "
50
+ continue
51
+ try:
52
+ if s == -1 and i != 0:
53
+ if plus:
54
+ plus = " "
55
+ tmp += plus + "- " + prx(i)
56
+ plus = " + "
57
+ continue
58
+ except:
59
+ pass
60
+ try:
61
+ if plus and s < 0:
62
+ tmp += " - " + str(-s) + " " + prx(i)
63
+ continue
64
+ except:
65
+ pass
66
+ tmp += plus + str(s) + " " + prx(i)
67
+ plus = " + "
68
+ return tmp.strip()
69
+
70
+ def __eq__(self, other):
71
+ if not isinstance(other, self.__class__):
72
+ return False
73
+ return self.coeff == other.coeff
74
+
75
+ def __bool__(self):
76
+ return bool(self.degree()) or bool(self.coeff[0])
77
+
78
+ def degree(self):
79
+ return len(self.coeff) - 1
80
+
81
+ def map(self, func):
82
+ self.coeff = list(map(func, self.coeff))
83
+
84
+ def __add__(self, other: "Poly") -> "Poly":
85
+ if not isinstance(other, self.__class__):
86
+ raise NotImplementedError(f"Cannot add {self} and {other}.")
87
+ ls, lo = len(self.coeff), len(other.coeff)
88
+ if ls < lo:
89
+ scoeff = self.coeff + (lo - ls) * [0]
90
+ else:
91
+ scoeff = self.coeff
92
+ if ls > lo:
93
+ ocoeff = other.coeff + (ls - lo) * [0]
94
+ else:
95
+ ocoeff = other.coeff
96
+ modulus = self.modulus
97
+ if not modulus and other.modulus:
98
+ modulus = other.modulus
99
+ return self.__class__([s + o for s, o in zip(scoeff, ocoeff)], modulus=modulus)
100
+
101
+ def __neg__(self) -> "Poly":
102
+ return Poly([-s for s in self.coeff], modulus=self.modulus)
103
+
104
+ def __sub__(self, other: "Poly") -> "Poly":
105
+ if not isinstance(other, self.__class__):
106
+ raise NotImplementedError(f"Cannot subtract {self} and {other}.")
107
+ ls, lo = len(self.coeff), len(other.coeff)
108
+ if ls < lo:
109
+ scoeff = self.coeff + (lo - ls) * [0]
110
+ else:
111
+ scoeff = self.coeff
112
+ if ls > lo:
113
+ ocoeff = other.coeff + (ls - lo) * [0]
114
+ else:
115
+ ocoeff = other.coeff
116
+ modulus = self.modulus
117
+ if not modulus and other.modulus:
118
+ modulus = other.modulus
119
+ return self.__class__([s - o for s, o in zip(scoeff, ocoeff)], modulus=modulus)
120
+
121
+ def __mul__(self, other: "Poly") -> "Poly":
122
+ if isinstance(other, int):
123
+ return Poly([other * s for s in self.coeff])
124
+ if not isinstance(other, self.__class__):
125
+ raise NotImplementedError(f"Cannot multiply {self} and {other}.")
126
+ ls, lo = len(self.coeff), len(other.coeff)
127
+ coeff = [0] * (ls + lo - 1)
128
+ for k in range(ls + lo - 1):
129
+ coeff[k] = sum(
130
+ [
131
+ self.coeff[j] * other.coeff[k - j]
132
+ for j in range(max(0, k - lo + 1), min(ls, k + 1))
133
+ ]
134
+ )
135
+ modulus = self.modulus
136
+ if not modulus and other.modulus:
137
+ modulus = other.modulus
138
+ return self.__class__(coeff, modulus=modulus)
139
+
140
+ def __rmul__(self, other: int) -> "Poly":
141
+ return self.__class__([other * s for s in self.coeff], modulus=self.modulus)
142
+
143
+ def __pow__(self, i: int) -> "Poly":
144
+ res = self.__class__([1], modulus=self.modulus)
145
+ if i < 0:
146
+ if not self.modulus:
147
+ raise NotImplementedError(f"Cannot divide.")
148
+ tmp = self.inv()
149
+ else:
150
+ tmp = self
151
+ for _ in range(i):
152
+ res *= tmp
153
+ return res
154
+
155
+ def divmod(self, other: "Poly") -> ("Poly", "Poly"):
156
+ if isinstance(other, list):
157
+ other = self.__class__(other)
158
+ elif not isinstance(other, self.__class__):
159
+ raise NotImplementedError(f"Cannot divide {self} and {other}.")
160
+ if not other:
161
+ raise ValueError(f"{other} must be nonzero.")
162
+ sd, od = self.degree(), other.degree()
163
+ if sd < od:
164
+ return self.__class__([0]), self
165
+ div = [0] * (sd - od + 1)
166
+ lco = other.coeff[-1]
167
+ if bool(lco - 1):
168
+ tmp = 1 / lco
169
+ oth = [c * tmp for c in other.coeff]
170
+ rem = [c * tmp for c in self.coeff]
171
+ else:
172
+ oth = other.coeff
173
+ rem = [c for c in self.coeff]
174
+ for i in range(sd - od + 1):
175
+ tmp = rem[sd - i]
176
+ div[sd - od - i] = tmp
177
+ for j in range(od + 1):
178
+ rem[sd - i - j] -= tmp * oth[od - j]
179
+ if bool(lco - 1):
180
+ rem = [c * lco for c in rem]
181
+ return self.__class__(div, modulus=self.modulus), self.__class__(
182
+ rem, modulus=self.modulus
183
+ )
184
+
185
+ def mod(self, other: "Poly") -> None:
186
+ if isinstance(other, list):
187
+ other = self.__class__(other)
188
+ elif not isinstance(other, self.__class__):
189
+ raise NotImplementedError(f"Cannot divide {self} and {other}.")
190
+ if not other:
191
+ raise NotImplementedError(f"{other} must be nonzero.")
192
+ sd, od = self.degree(), other.degree()
193
+ if sd < od:
194
+ return self
195
+ lco = other.coeff[-1]
196
+ if bool(lco - 1):
197
+ tmp = 1 / lco
198
+ oth = [c * tmp for c in other.coeff]
199
+ else:
200
+ oth = other.coeff
201
+ for i in range(sd - od + 1):
202
+ tmp = self.coeff[sd - i]
203
+ for j in range(od + 1):
204
+ self.coeff[sd - i - j] -= tmp * oth[od - j]
205
+ for i in range(len(self.coeff) - 1, 0, -1):
206
+ if self.coeff[i]:
207
+ break
208
+ self.coeff.pop(i)
209
+
210
+ def inv(self, other: "Poly" = None) -> "Poly":
211
+ if not other:
212
+ other = self.modulus
213
+ if isinstance(other, list):
214
+ other = self.__class__(other)
215
+ elif not isinstance(other, self.__class__):
216
+ raise NotImplementedError(f"Cannot invert {self} modulo {other}.")
217
+ if not other:
218
+ raise NotImplementedError(f"{other} must be nonzero.")
219
+ r0, r1 = other, self
220
+ y0, y1 = self.__class__([0], modulus=self.modulus), self.__class__(
221
+ [1], modulus=self.modulus
222
+ )
223
+ while r1:
224
+ q, r = r0.divmod(r1)
225
+ r0, r1 = r1, r
226
+ y0, y1 = y1, y0 - q * y1
227
+ if r0.degree() != 0:
228
+ raise ValueError(f"{self} is not invertible mod {other}.")
229
+ tmp = 1 / r0[0]
230
+ for i in range(y0.degree()):
231
+ y0.coeff[i] *= tmp
232
+ return y0