kryptools 1.8__tar.gz → 1.8.2__tar.gz
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-1.8 → kryptools-1.8.2}/PKG-INFO +3 -3
- {kryptools-1.8 → kryptools-1.8.2}/README.md +2 -2
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/Zmod.py +10 -4
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/__init__.py +1 -1
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/la.py +275 -61
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/lat.py +56 -59
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/nt.py +18 -1
- {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/PKG-INFO +3 -3
- {kryptools-1.8 → kryptools-1.8.2}/pyproject.toml +1 -1
- kryptools-1.8.2/tests/test_la.py +193 -0
- kryptools-1.8.2/tests/test_lat.py +49 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_nt.py +9 -0
- kryptools-1.8/tests/test_la.py +0 -92
- kryptools-1.8/tests/test_lat.py +0 -17
- {kryptools-1.8 → kryptools-1.8.2}/LICENSE +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/GF2.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/aes.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/blockcipher.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/code.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/conway_polynomials.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/des.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_bsgs.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_ic.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_qs.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_rho.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/ec.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_dix.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_ecm.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_fmt.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_pm1.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_qs.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_rho.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/intfuncs.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/keccak.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/poly.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/primes.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools/sha1.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/SOURCES.txt +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/dependency_links.txt +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/top_level.txt +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/setup.cfg +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_GF2.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_Zmod.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_aes.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_code.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_des.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_dlog.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_ec.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor_fmt.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor_pm1.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor_qs.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_intfuncs.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_keccak.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_poly.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_primes.py +0 -0
- {kryptools-1.8 → kryptools-1.8.2}/tests/test_sha1.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kryptools
|
|
3
|
-
Version: 1.8
|
|
3
|
+
Version: 1.8.2
|
|
4
4
|
Summary: Implemenation of same basic algorithms used in cryptography.
|
|
5
5
|
Author-email: Gerald Teschl <gerald.teschl@univie.ac.at>
|
|
6
6
|
License: MIT
|
|
@@ -43,11 +43,11 @@ The tools contained are:
|
|
|
43
43
|
* solvers for discrete logarithms (naive, Pollard rho, Shanks baby step/giant step, index calculus, quadratic sieve)
|
|
44
44
|
* integer factorization (Fermat, Pollard p-1, Pollard rho, Lenstra's ECM, Dixon, basic quadratic sieve)
|
|
45
45
|
* elliptic curves (Weierstrass form), group operations, order, discrete logarithms
|
|
46
|
-
* linear algebra: Hermite normal form, Gram-Schmidt
|
|
46
|
+
* linear algebra: Hermite normal form, Smith normal form, Gram-Schmidt
|
|
47
47
|
* lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz), SIS, ISIS, LWE
|
|
48
48
|
* linear codes: Hamming distance, left standard form, parity check matrix
|
|
49
49
|
|
|
50
|
-
* Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, etc.)
|
|
50
|
+
* Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, solving linear systems, etc.)
|
|
51
51
|
* BinaryMatrix: a class for Matrices with binary entries (for much faster row operations)
|
|
52
52
|
* Poly: a class for polynomials (division, modulo, factoring and irreducibility test over finite fields, Lagrange interpolation)
|
|
53
53
|
* Zmod: a class for the ring of integers modulo an integer
|
|
@@ -25,11 +25,11 @@ The tools contained are:
|
|
|
25
25
|
* solvers for discrete logarithms (naive, Pollard rho, Shanks baby step/giant step, index calculus, quadratic sieve)
|
|
26
26
|
* integer factorization (Fermat, Pollard p-1, Pollard rho, Lenstra's ECM, Dixon, basic quadratic sieve)
|
|
27
27
|
* elliptic curves (Weierstrass form), group operations, order, discrete logarithms
|
|
28
|
-
* linear algebra: Hermite normal form, Gram-Schmidt
|
|
28
|
+
* linear algebra: Hermite normal form, Smith normal form, Gram-Schmidt
|
|
29
29
|
* lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz), SIS, ISIS, LWE
|
|
30
30
|
* linear codes: Hamming distance, left standard form, parity check matrix
|
|
31
31
|
|
|
32
|
-
* Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, etc.)
|
|
32
|
+
* Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, solving linear systems, etc.)
|
|
33
33
|
* BinaryMatrix: a class for Matrices with binary entries (for much faster row operations)
|
|
34
34
|
* Poly: a class for polynomials (division, modulo, factoring and irreducibility test over finite fields, Lagrange interpolation)
|
|
35
35
|
* Zmod: a class for the ring of integers modulo an integer
|
|
@@ -172,7 +172,7 @@ class ZmodPoint:
|
|
|
172
172
|
return f"{x} (mod {self.ring.n})"
|
|
173
173
|
|
|
174
174
|
def __eq__(self, other):
|
|
175
|
-
if not isinstance(other, self.__class__) or self.ring != other.ring:
|
|
175
|
+
if not isinstance(other, self.__class__) or self.ring.n != other.ring.n:
|
|
176
176
|
return False
|
|
177
177
|
return self.x == other.x
|
|
178
178
|
|
|
@@ -328,18 +328,24 @@ class ZmodPoint:
|
|
|
328
328
|
roots.append((p**(j//2) * x) % pk)
|
|
329
329
|
return self.ring(crt(roots, powers))
|
|
330
330
|
|
|
331
|
-
def solve(self, b):
|
|
331
|
+
def solve(self, b, all_solutions: bool = False) -> "ZmodPoint":
|
|
332
332
|
"Find a solution `x` of the linear equation `self * x == b` in Z_n."
|
|
333
333
|
b = int(b)
|
|
334
334
|
if not self:
|
|
335
335
|
if b:
|
|
336
336
|
return None
|
|
337
|
+
if all_solutions:
|
|
338
|
+
return list(self.ring)
|
|
337
339
|
return self.ring(0)
|
|
338
340
|
g = gcd(self.x, self.ring.n)
|
|
339
341
|
if b % g:
|
|
340
342
|
return None
|
|
341
|
-
|
|
343
|
+
m = self.ring.n // g
|
|
344
|
+
sol = self.ring(pow(self.x // g, -1, m) * (b // g) % m)
|
|
345
|
+
if all_solutions:
|
|
346
|
+
return [ sol + j * m for j in range(g) ]
|
|
347
|
+
return sol
|
|
342
348
|
|
|
343
|
-
def is_generator(self):
|
|
349
|
+
def is_generator(self) -> bool:
|
|
344
350
|
"Test if the point is a generator of the group Z_n^*."
|
|
345
351
|
return self.ring.order() == self.order()
|
|
@@ -5,7 +5,7 @@ Implemenation of same basic algorithms used in cryptography.
|
|
|
5
5
|
__author__ = "Gerald Teschl"
|
|
6
6
|
__copyright__ = "Copyright 2024-2025, Gerald Teschl"
|
|
7
7
|
__license__ = "MIT License"
|
|
8
|
-
__version__ = "1.8"
|
|
8
|
+
__version__ = "1.8.2"
|
|
9
9
|
__email__ = "Gerald.Teschl@univie.ac.at"
|
|
10
10
|
|
|
11
11
|
from .nt import egcd, cf, convergents, legendre_symbol, jacobi_symbol, sqrt_mod, euler_phi, carmichael_lambda, moebius_mu, is_carmichael_number, order, crt
|
|
@@ -6,6 +6,7 @@ Linear algebra
|
|
|
6
6
|
from math import gcd, inf, sqrt
|
|
7
7
|
from numbers import Number
|
|
8
8
|
from fractions import Fraction
|
|
9
|
+
from .nt import egcd
|
|
9
10
|
from .Zmod import Zmod
|
|
10
11
|
|
|
11
12
|
class Matrix:
|
|
@@ -215,19 +216,66 @@ class Matrix:
|
|
|
215
216
|
self.matrix[i].append(c)
|
|
216
217
|
self.cols += 1
|
|
217
218
|
|
|
218
|
-
def
|
|
219
|
+
def swap_columns(self, i: int, j: int) -> None:
|
|
220
|
+
"Swap two columns."
|
|
221
|
+
for k in range(self.rows):
|
|
222
|
+
self.matrix[k][i], self.matrix[k][j] = self.matrix[k][j], self.matrix[k][i]
|
|
223
|
+
|
|
224
|
+
def swap_rows(self, i: int, j: int) -> None:
|
|
225
|
+
"Swap two rows."
|
|
226
|
+
self.matrix[i], self.matrix[j] = self.matrix[j], self.matrix[i]
|
|
227
|
+
|
|
228
|
+
def permute_columns(self, permutation: list) -> None:
|
|
219
229
|
"Permute columns according to a list of new positions."
|
|
220
230
|
if len(permutation) != self.cols:
|
|
221
231
|
raise ValueError(f"The argument must be a list of indices of length {self.cols}.")
|
|
222
232
|
for i in range(self.rows):
|
|
223
233
|
self.matrix[i] = [ self.matrix[i][j] for j in permutation]
|
|
224
234
|
|
|
225
|
-
def permute_rows(self, permutation) -> None:
|
|
235
|
+
def permute_rows(self, permutation: list) -> None:
|
|
226
236
|
"Permute rows according to a list of new positions."
|
|
227
237
|
if len(permutation) != self.rows:
|
|
228
238
|
raise ValueError(f"The argument must be a list of indices of length {self.rows}.")
|
|
229
239
|
self.matrix = [ self.matrix[i] for i in permutation]
|
|
230
240
|
|
|
241
|
+
def scale_column(self, i: int, a: Number) -> None:
|
|
242
|
+
"Scale a column."
|
|
243
|
+
for k in range(self.rows):
|
|
244
|
+
self.matrix[k][i] *= a
|
|
245
|
+
|
|
246
|
+
def scale_row(self, i: int, a: Number) -> None:
|
|
247
|
+
"Scale a row."
|
|
248
|
+
for k in range(self.cols):
|
|
249
|
+
self.matrix[i][k] *= a
|
|
250
|
+
|
|
251
|
+
def addto_column(self, i: int, j: int, a: Number|None = None) -> None:
|
|
252
|
+
"Add a multiple of the first column to the second."
|
|
253
|
+
if a is None:
|
|
254
|
+
for k in range(self.rows):
|
|
255
|
+
self.matrix[k][i] += self.matrix[k][j]
|
|
256
|
+
else:
|
|
257
|
+
for k in range(self.rows):
|
|
258
|
+
self.matrix[k][i] += a * self.matrix[k][j]
|
|
259
|
+
|
|
260
|
+
def addto_row(self, i: int, j: int, a: Number|None = None) -> None:
|
|
261
|
+
"Add a multiple of the first row to the second."
|
|
262
|
+
if a is None:
|
|
263
|
+
for k in range(self.cols):
|
|
264
|
+
self.matrix[i][k] += self.matrix[j][k]
|
|
265
|
+
else:
|
|
266
|
+
for k in range(self.cols):
|
|
267
|
+
self.matrix[i][k] += a * self.matrix[j][k]
|
|
268
|
+
|
|
269
|
+
def maop_row(self, i: int, k: int, a: Number, b: Number, c: Number, d: Number) -> None:
|
|
270
|
+
"Replaces the i,j and k,j entry by by using an matrix column operation."
|
|
271
|
+
for l in range(self.cols):
|
|
272
|
+
self.matrix[i][l], self.matrix[k][l] = a * self.matrix[i][l] + b * self.matrix[k][l], c * self.matrix[i][l] + d * self.matrix[k][l]
|
|
273
|
+
|
|
274
|
+
def maop_column(self, j: int, k: int, a: Number, b: Number, c: Number, d: Number) -> None:
|
|
275
|
+
"Replaces the i,j and i,k entry by using an matrix column operation."
|
|
276
|
+
for l in range(self.rows):
|
|
277
|
+
self.matrix[l][j], self.matrix[l][k] = a * self.matrix[l][j] + b * self.matrix[l][k], c * self.matrix[l][j] + d * self.matrix[l][k]
|
|
278
|
+
|
|
231
279
|
def __eq__(self, other):
|
|
232
280
|
if not isinstance(other, self.__class__):
|
|
233
281
|
return False
|
|
@@ -381,72 +429,71 @@ class Matrix:
|
|
|
381
429
|
"Compute the reduced echelon form if the base ring is Z_n and no field."
|
|
382
430
|
ring = self.matrix[0][0].ring
|
|
383
431
|
M = self.applyfunc(int)
|
|
384
|
-
done =
|
|
385
|
-
while
|
|
386
|
-
M = M.
|
|
387
|
-
done =
|
|
432
|
+
done = start
|
|
433
|
+
while done < M.cols:
|
|
434
|
+
M = M.hrnf(start = start, drop_zero_rows = drop_zero_rows)
|
|
435
|
+
done = M.cols
|
|
388
436
|
for i, j in enumerate(M.pivotcols):
|
|
437
|
+
if j < start:
|
|
438
|
+
continue
|
|
389
439
|
pivot = M.matrix[i][j]
|
|
390
440
|
if pivot >= ring.n:
|
|
391
|
-
done =
|
|
441
|
+
done = min(j, done)
|
|
392
442
|
pivot %= ring.n
|
|
393
|
-
if pivot
|
|
394
|
-
|
|
395
|
-
|
|
443
|
+
if pivot: # find the invertible part
|
|
444
|
+
g = gcd(pivot, ring.n)
|
|
445
|
+
while g > 1:
|
|
446
|
+
pivot //= g
|
|
447
|
+
g = gcd(pivot, ring.n)
|
|
448
|
+
if pivot > 1: # we can make the pivot smaller
|
|
449
|
+
done = min(j, done)
|
|
396
450
|
tmp = pow(pivot, -1, ring.n)
|
|
397
|
-
for k in range(j
|
|
451
|
+
for k in range(j, M.cols):
|
|
398
452
|
M.matrix[i][k] *= tmp
|
|
399
453
|
M.map(lambda x: x % ring.n)
|
|
400
454
|
M.map(ring)
|
|
401
455
|
return M
|
|
402
456
|
|
|
403
|
-
|
|
457
|
+
|
|
458
|
+
def hrnf(self, start = 0, drop_zero_rows: bool = True) -> "Matrix":
|
|
404
459
|
"Compute the Hermite row normal form."
|
|
405
460
|
n, m = self.cols, self.rows
|
|
406
461
|
if not isinstance(self.matrix[0][0], int):
|
|
407
462
|
raise ValueError("Hermite normal form requires integer entries!")
|
|
408
|
-
H =
|
|
409
|
-
pivotcols = []
|
|
410
|
-
nonpivotcols = []
|
|
463
|
+
H = self[:,:]
|
|
464
|
+
H.pivotcols = []
|
|
465
|
+
H.nonpivotcols = []
|
|
411
466
|
i = 0
|
|
412
467
|
if start >= n:
|
|
413
468
|
raise ValueError("Start value cannot be beyond the last column.")
|
|
414
469
|
for j in range(start, n):
|
|
415
470
|
i0 = i
|
|
416
|
-
minimum = abs(H[i][j]) # search for the pivot in the present column
|
|
471
|
+
minimum = abs(H.matrix[i][j]) # search for the pivot in the present column
|
|
417
472
|
for ii in range(i + 1, m):
|
|
418
|
-
tmp = abs(H[ii][j])
|
|
473
|
+
tmp = abs(H.matrix[ii][j])
|
|
419
474
|
if tmp > 0 and (tmp < minimum or minimum == 0):
|
|
420
475
|
minimum = tmp
|
|
421
476
|
i0 = ii
|
|
422
477
|
if minimum == 0:
|
|
423
|
-
nonpivotcols.append(j)
|
|
478
|
+
H.nonpivotcols.append(j)
|
|
424
479
|
continue # all entrjes are zero
|
|
425
|
-
pivotcols.append(j)
|
|
480
|
+
H.pivotcols.append(j)
|
|
426
481
|
if i0 > i:
|
|
427
|
-
H
|
|
428
|
-
if H[i][j] < 0:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
H
|
|
436
|
-
if H[ii][j]:
|
|
437
|
-
H[i], H[ii] = H[ii], H[i] # swap rows
|
|
438
|
-
else:
|
|
439
|
-
ii += 1
|
|
482
|
+
H.swap_rows(i, i0) # swap rows, to move the pivot in place
|
|
483
|
+
if H.matrix[i][j] < 0:
|
|
484
|
+
H.scale_row(i, -1) # make the pivot positive
|
|
485
|
+
for ii in range(i + 1, m): # make the column entries below to the pivot zero
|
|
486
|
+
if H.matrix[ii][j]:
|
|
487
|
+
g, x, y = egcd(H.matrix[i][j], H.matrix[ii][j], minimal = True)
|
|
488
|
+
xx = H.matrix[i][j] // g
|
|
489
|
+
yy = - H.matrix[ii][j] // g
|
|
490
|
+
H.maop_row(i, ii, x, y, yy, xx)
|
|
440
491
|
for ii in range(i): # reduce the column entries above to the pivot
|
|
441
|
-
|
|
442
|
-
for k in range(n):
|
|
443
|
-
H[ii][k] -= tmp * H[i][k]
|
|
492
|
+
H.addto_row(ii, i, -(H.matrix[ii][j] // H.matrix[i][j]))
|
|
444
493
|
i += 1
|
|
445
494
|
if i >= m:
|
|
446
495
|
break
|
|
447
|
-
H
|
|
448
|
-
H.pivotcols = pivotcols
|
|
449
|
-
H.nonpivotcols = nonpivotcols + list(range(j+1, H.cols))
|
|
496
|
+
H.nonpivotcols += list(range(j+1, H.cols))
|
|
450
497
|
# purge zero rows
|
|
451
498
|
if drop_zero_rows:
|
|
452
499
|
l = len(H.pivotcols)
|
|
@@ -456,6 +503,141 @@ class Matrix:
|
|
|
456
503
|
H.rows = l
|
|
457
504
|
return H
|
|
458
505
|
|
|
506
|
+
def hnf(self, drop_zero_columns: bool = True) -> "Matrix":
|
|
507
|
+
"Compute the Hermite normal form."
|
|
508
|
+
n, m = self.cols, self.rows
|
|
509
|
+
if not isinstance(self.matrix[0][0], int):
|
|
510
|
+
raise ValueError("Hermite normal form requires integer entries!")
|
|
511
|
+
H = self[:,:]
|
|
512
|
+
j = n - 1
|
|
513
|
+
for i in range(m-1,-1,-1):
|
|
514
|
+
j0 = j
|
|
515
|
+
minimum = abs(H.matrix[i][j]) # search for the pivot in the present row
|
|
516
|
+
for jj in range(j):
|
|
517
|
+
tmp = abs(H.matrix[i][jj])
|
|
518
|
+
if tmp > 0 and (tmp < minimum or minimum == 0):
|
|
519
|
+
minimum = tmp
|
|
520
|
+
j0 = jj
|
|
521
|
+
if minimum == 0:
|
|
522
|
+
continue # all entries are zero
|
|
523
|
+
if j0 < j:
|
|
524
|
+
H.swap_columns(j, j0) # swap columns, to move the pivot in place
|
|
525
|
+
if H.matrix[i][j] < 0:
|
|
526
|
+
H.scale_column(j, -1) # make the pivot positive
|
|
527
|
+
for jj in range(j): # make the row left to the pivot zero
|
|
528
|
+
if H.matrix[i][jj]:
|
|
529
|
+
g, x, y = egcd(H.matrix[i][j], H.matrix[i][jj], minimal = True)
|
|
530
|
+
xx = H.matrix[i][j] // g
|
|
531
|
+
yy = - H.matrix[i][jj] // g
|
|
532
|
+
H.maop_column(j, jj, x, y, yy, xx)
|
|
533
|
+
for jj in range(j + 1, n): # reduce the row entries right to the pivot
|
|
534
|
+
H.addto_column(jj, j, -(H.matrix[i][jj] // H.matrix[i][j]))
|
|
535
|
+
j -= 1
|
|
536
|
+
if j < 0:
|
|
537
|
+
break
|
|
538
|
+
if drop_zero_columns:
|
|
539
|
+
while H.cols > 1 and all(not H.matrix[i][0] for i in range(m)): # remove zero columns
|
|
540
|
+
H = H[:, 1:]
|
|
541
|
+
return H
|
|
542
|
+
|
|
543
|
+
def snf(self, drop_zero_rows: bool = False, drop_zero_columns: bool = False, include_S: bool = True, include_T: bool = True) -> "Matrix":
|
|
544
|
+
"Computes the Smith normal form D of a matrix A with integer coefficients together with invertible matrices S and T such that D = S * A * T."
|
|
545
|
+
if not isinstance(self.matrix[0][0], int):
|
|
546
|
+
raise ValueError("Smith normal form requires integer entries!")
|
|
547
|
+
m, n = self.rows, self.cols
|
|
548
|
+
A = self[:,:]
|
|
549
|
+
if include_T:
|
|
550
|
+
T = A.eye(n)
|
|
551
|
+
if include_S:
|
|
552
|
+
S = A.eye(m)
|
|
553
|
+
for i in range(min(n,m)):
|
|
554
|
+
minimum = abs(A.matrix[i][i]) # search for the pivot
|
|
555
|
+
i0, j0 = i, i
|
|
556
|
+
for ii in range(i, m):
|
|
557
|
+
for jj in range(i, n):
|
|
558
|
+
tmp = abs(A.matrix[ii][jj])
|
|
559
|
+
if tmp > 0 and (tmp < minimum or minimum == 0):
|
|
560
|
+
minimum = tmp
|
|
561
|
+
i0, j0 = ii, jj
|
|
562
|
+
if not minimum:
|
|
563
|
+
break
|
|
564
|
+
if j0 != i: # swap columns, to move the pivot in place
|
|
565
|
+
if include_T:
|
|
566
|
+
T.swap_columns(i, j0)
|
|
567
|
+
A.swap_columns(i, j0)
|
|
568
|
+
if i0 != i: # swap rows, to move the pivot in place
|
|
569
|
+
if include_S:
|
|
570
|
+
S.swap_rows(i, i0)
|
|
571
|
+
A.swap_rows(i, i0)
|
|
572
|
+
if A[i, i] < 0: # make the pivot positive
|
|
573
|
+
if include_S:
|
|
574
|
+
S.scale_row(i, -1)
|
|
575
|
+
A.scale_row(i, -1)
|
|
576
|
+
done = False
|
|
577
|
+
while not done:
|
|
578
|
+
for jj in range(i+1, n):
|
|
579
|
+
if A.matrix[i][jj] == 0:
|
|
580
|
+
continue
|
|
581
|
+
g, x, y = egcd(A.matrix[i][i], A.matrix[i][jj], minimal = True)
|
|
582
|
+
xx = A.matrix[i][i] // g
|
|
583
|
+
yy = A.matrix[i][jj] // g
|
|
584
|
+
if include_T:
|
|
585
|
+
T.maop_column(i, jj, x, y, -yy, xx)
|
|
586
|
+
A.maop_column(i, jj, x, y, -yy, xx)
|
|
587
|
+
for ii in range(i+1, m):
|
|
588
|
+
if A.matrix[ii][i] == 0:
|
|
589
|
+
continue
|
|
590
|
+
g, x, y = egcd(A.matrix[i][i], A.matrix[ii][i], minimal = True)
|
|
591
|
+
xx = A.matrix[i][i] // g
|
|
592
|
+
yy = -A.matrix[ii][i] // g
|
|
593
|
+
if include_S:
|
|
594
|
+
S.maop_row(i, ii, x, y, yy, xx)
|
|
595
|
+
A.maop_row(i, ii, x, y, yy, xx)
|
|
596
|
+
done = True
|
|
597
|
+
for jj in range(i+1, n):
|
|
598
|
+
if A[i,jj] != 0:
|
|
599
|
+
done = False
|
|
600
|
+
break
|
|
601
|
+
for l in range(min(n,m)-1, 0, -1):
|
|
602
|
+
for i in range(l):
|
|
603
|
+
if A.matrix[i][i] and A.matrix[i+1][i+1] % A.matrix[i][i]:
|
|
604
|
+
g, x, y = egcd(A.matrix[i][i], A.matrix[i+1][i+1], minimal = True)
|
|
605
|
+
aa= A.matrix[i][i] //g
|
|
606
|
+
bb = -A.matrix[i+1][i+1]//g
|
|
607
|
+
by = bb * y
|
|
608
|
+
if include_T:
|
|
609
|
+
T.addto_column(i, i+1)
|
|
610
|
+
T.addto_column(i+1, i, by)
|
|
611
|
+
A.addto_column(i, i+1)
|
|
612
|
+
A.addto_column(i+1, i, by)
|
|
613
|
+
if include_S:
|
|
614
|
+
S.maop_row(i, i+1, x, y, bb, aa)
|
|
615
|
+
A.maop_row(i, i+1, x, y, bb, aa)
|
|
616
|
+
if drop_zero_rows or drop_zero_columns:
|
|
617
|
+
for i in range(min(n,m)):
|
|
618
|
+
if A[i,i] == 0:
|
|
619
|
+
break
|
|
620
|
+
else:
|
|
621
|
+
i += 1
|
|
622
|
+
i = max(i, 1)
|
|
623
|
+
if drop_zero_rows:
|
|
624
|
+
if i < m:
|
|
625
|
+
del A[i:, :]
|
|
626
|
+
if include_S:
|
|
627
|
+
del S[i:, :]
|
|
628
|
+
if drop_zero_columns:
|
|
629
|
+
if i < n:
|
|
630
|
+
del A[:, i:]
|
|
631
|
+
if include_T:
|
|
632
|
+
del T[:, i:]
|
|
633
|
+
if include_S and include_T:
|
|
634
|
+
return A, S, T
|
|
635
|
+
if include_S:
|
|
636
|
+
return A, S
|
|
637
|
+
if include_T:
|
|
638
|
+
return A, T
|
|
639
|
+
return A
|
|
640
|
+
|
|
459
641
|
def left_standard_form(self) -> "Matrix":
|
|
460
642
|
"Compute the left standard form."
|
|
461
643
|
# reduced row echelon form
|
|
@@ -468,7 +650,24 @@ class Matrix:
|
|
|
468
650
|
def kernel(self) -> "Matrix":
|
|
469
651
|
"Compute a basis for the kernel."
|
|
470
652
|
if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
|
|
471
|
-
|
|
653
|
+
ring = self.matrix[0][0].ring
|
|
654
|
+
M = self.applyfunc(int)
|
|
655
|
+
D, T = M.snf(include_S=False, drop_zero_rows=True)
|
|
656
|
+
D.map(ring)
|
|
657
|
+
T.map(ring)
|
|
658
|
+
K = D.eye(D.cols)
|
|
659
|
+
shift = 0
|
|
660
|
+
for i in range(D.rows):
|
|
661
|
+
g = gcd(int(D.matrix[i][i]), ring.n)
|
|
662
|
+
if g == 1:
|
|
663
|
+
if shift == D.cols - 1:
|
|
664
|
+
K.matrix[i][i - shift] = ring(0)
|
|
665
|
+
break
|
|
666
|
+
K.delete_columns(i - shift)
|
|
667
|
+
shift += 1
|
|
668
|
+
elif g < ring.n:
|
|
669
|
+
K.matrix[i][i-shift] = ring(ring.n // g)
|
|
670
|
+
return T * K
|
|
472
671
|
_, one = self._guess_zero()
|
|
473
672
|
M = self.rref(drop_zero_rows = True)
|
|
474
673
|
K = M.zeros(M.cols, max(1,len(M.nonpivotcols)))
|
|
@@ -553,32 +752,36 @@ class Matrix:
|
|
|
553
752
|
def solve(self, b: list|tuple) -> "Matrix":
|
|
554
753
|
"Solve the linear system with given inhomogenous vector."
|
|
555
754
|
if isinstance(b, list|tuple):
|
|
556
|
-
b =
|
|
755
|
+
b = self.__class__(b)
|
|
557
756
|
if self.rows != b.rows or b.cols != 1:
|
|
558
757
|
raise ValueError("Matrix dimensions do not match.")
|
|
559
|
-
A = self.zeros(self.rows, self.cols + 1) # extended coefficient matrix
|
|
560
|
-
A[:, 0:self.cols] = self
|
|
561
|
-
A[:, self.cols] = b
|
|
562
|
-
A = A.rref(drop_zero_rows = True)
|
|
563
|
-
solution = self.zeros(self.cols, 1)
|
|
564
|
-
if not any(A.matrix[-1][:-1]):
|
|
565
|
-
if A.matrix[-1][-1]:
|
|
566
|
-
return None # Not solvable
|
|
567
758
|
if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
|
|
568
759
|
# the matrix is over a ring (not a field)
|
|
569
760
|
ring = self.matrix[0][0].ring
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
761
|
+
A = self.applyfunc(int)
|
|
762
|
+
D, S, T = A.snf()
|
|
763
|
+
D.map(ring)
|
|
764
|
+
S.map(ring)
|
|
765
|
+
T.map(ring)
|
|
766
|
+
b = S * b
|
|
767
|
+
for i in range(min(A.rows, A.cols), A.rows):
|
|
768
|
+
if b[i]:
|
|
769
|
+
return None
|
|
770
|
+
y = [ ring(0) ] * A.cols
|
|
771
|
+
for i in range(min(A.rows, A.cols)):
|
|
772
|
+
y[i] = D[i,i].solve(b[i])
|
|
773
|
+
if y[i] is None:
|
|
580
774
|
return None
|
|
581
|
-
|
|
775
|
+
return T * Matrix(y)
|
|
776
|
+
# the matrix is over a field
|
|
777
|
+
A = self[:,:]
|
|
778
|
+
A.append_column(b)
|
|
779
|
+
A = A.rref(drop_zero_rows = True)
|
|
780
|
+
if not any(A.matrix[-1][:-1]) and A.matrix[-1][-1]:
|
|
781
|
+
return None # Not solvable
|
|
782
|
+
solution = self.zeros(self.cols, 1)
|
|
783
|
+
for i, j in enumerate(A.pivotcols):
|
|
784
|
+
solution[j] = A.matrix[i][-1]
|
|
582
785
|
return solution
|
|
583
786
|
|
|
584
787
|
def is_unimodular(self) -> bool:
|
|
@@ -879,7 +1082,18 @@ class BinaryMatrix:
|
|
|
879
1082
|
self.rows += 1
|
|
880
1083
|
self.matrix.append(row)
|
|
881
1084
|
|
|
882
|
-
def
|
|
1085
|
+
def swap_columns(self, i: int, j: int) -> None:
|
|
1086
|
+
"Swap two columns."
|
|
1087
|
+
for k in range(self.rows):
|
|
1088
|
+
b = self.matrix[k]
|
|
1089
|
+
x = ((b >> i) ^ (b >> j)) & 1
|
|
1090
|
+
self.matrix[k] ^= ((x << i) | (x << j))
|
|
1091
|
+
|
|
1092
|
+
def swap_rows(self, i: int, j: int) -> None:
|
|
1093
|
+
"Swap two rows."
|
|
1094
|
+
self.matrix[i], self.matrix[j] = self.matrix[j], self.matrix[i]
|
|
1095
|
+
|
|
1096
|
+
def permute_columns(self, permutation: list) -> None:
|
|
883
1097
|
"Permute columns according to a list of new positions."
|
|
884
1098
|
if len(permutation) != self.cols:
|
|
885
1099
|
raise ValueError(f"The argument must be a list of indices of length {self.cols}.")
|
|
@@ -888,7 +1102,7 @@ class BinaryMatrix:
|
|
|
888
1102
|
bits = [ bits[j] for j in permutation]
|
|
889
1103
|
self.matrix[i] = self.from_bits(bits)
|
|
890
1104
|
|
|
891
|
-
def permute_rows(self, permutation) -> None:
|
|
1105
|
+
def permute_rows(self, permutation: list) -> None:
|
|
892
1106
|
"Permute rows according to a list of new positions."
|
|
893
1107
|
if len(permutation) != self.rows:
|
|
894
1108
|
raise ValueError(f"The argument must be a list of indices of length {self.rows}.")
|