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.
Files changed (59) hide show
  1. {kryptools-1.8 → kryptools-1.8.2}/PKG-INFO +3 -3
  2. {kryptools-1.8 → kryptools-1.8.2}/README.md +2 -2
  3. {kryptools-1.8 → kryptools-1.8.2}/kryptools/Zmod.py +10 -4
  4. {kryptools-1.8 → kryptools-1.8.2}/kryptools/__init__.py +1 -1
  5. {kryptools-1.8 → kryptools-1.8.2}/kryptools/la.py +275 -61
  6. {kryptools-1.8 → kryptools-1.8.2}/kryptools/lat.py +56 -59
  7. {kryptools-1.8 → kryptools-1.8.2}/kryptools/nt.py +18 -1
  8. {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/PKG-INFO +3 -3
  9. {kryptools-1.8 → kryptools-1.8.2}/pyproject.toml +1 -1
  10. kryptools-1.8.2/tests/test_la.py +193 -0
  11. kryptools-1.8.2/tests/test_lat.py +49 -0
  12. {kryptools-1.8 → kryptools-1.8.2}/tests/test_nt.py +9 -0
  13. kryptools-1.8/tests/test_la.py +0 -92
  14. kryptools-1.8/tests/test_lat.py +0 -17
  15. {kryptools-1.8 → kryptools-1.8.2}/LICENSE +0 -0
  16. {kryptools-1.8 → kryptools-1.8.2}/kryptools/GF2.py +0 -0
  17. {kryptools-1.8 → kryptools-1.8.2}/kryptools/aes.py +0 -0
  18. {kryptools-1.8 → kryptools-1.8.2}/kryptools/blockcipher.py +0 -0
  19. {kryptools-1.8 → kryptools-1.8.2}/kryptools/code.py +0 -0
  20. {kryptools-1.8 → kryptools-1.8.2}/kryptools/conway_polynomials.py +0 -0
  21. {kryptools-1.8 → kryptools-1.8.2}/kryptools/des.py +0 -0
  22. {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp.py +0 -0
  23. {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_bsgs.py +0 -0
  24. {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_ic.py +0 -0
  25. {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_qs.py +0 -0
  26. {kryptools-1.8 → kryptools-1.8.2}/kryptools/dlp_rho.py +0 -0
  27. {kryptools-1.8 → kryptools-1.8.2}/kryptools/ec.py +0 -0
  28. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor.py +0 -0
  29. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_dix.py +0 -0
  30. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_ecm.py +0 -0
  31. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_fmt.py +0 -0
  32. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_pm1.py +0 -0
  33. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_qs.py +0 -0
  34. {kryptools-1.8 → kryptools-1.8.2}/kryptools/factor_rho.py +0 -0
  35. {kryptools-1.8 → kryptools-1.8.2}/kryptools/intfuncs.py +0 -0
  36. {kryptools-1.8 → kryptools-1.8.2}/kryptools/keccak.py +0 -0
  37. {kryptools-1.8 → kryptools-1.8.2}/kryptools/poly.py +0 -0
  38. {kryptools-1.8 → kryptools-1.8.2}/kryptools/primes.py +0 -0
  39. {kryptools-1.8 → kryptools-1.8.2}/kryptools/sha1.py +0 -0
  40. {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/SOURCES.txt +0 -0
  41. {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/dependency_links.txt +0 -0
  42. {kryptools-1.8 → kryptools-1.8.2}/kryptools.egg-info/top_level.txt +0 -0
  43. {kryptools-1.8 → kryptools-1.8.2}/setup.cfg +0 -0
  44. {kryptools-1.8 → kryptools-1.8.2}/tests/test_GF2.py +0 -0
  45. {kryptools-1.8 → kryptools-1.8.2}/tests/test_Zmod.py +0 -0
  46. {kryptools-1.8 → kryptools-1.8.2}/tests/test_aes.py +0 -0
  47. {kryptools-1.8 → kryptools-1.8.2}/tests/test_code.py +0 -0
  48. {kryptools-1.8 → kryptools-1.8.2}/tests/test_des.py +0 -0
  49. {kryptools-1.8 → kryptools-1.8.2}/tests/test_dlog.py +0 -0
  50. {kryptools-1.8 → kryptools-1.8.2}/tests/test_ec.py +0 -0
  51. {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor.py +0 -0
  52. {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor_fmt.py +0 -0
  53. {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor_pm1.py +0 -0
  54. {kryptools-1.8 → kryptools-1.8.2}/tests/test_factor_qs.py +0 -0
  55. {kryptools-1.8 → kryptools-1.8.2}/tests/test_intfuncs.py +0 -0
  56. {kryptools-1.8 → kryptools-1.8.2}/tests/test_keccak.py +0 -0
  57. {kryptools-1.8 → kryptools-1.8.2}/tests/test_poly.py +0 -0
  58. {kryptools-1.8 → kryptools-1.8.2}/tests/test_primes.py +0 -0
  59. {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
- return self.ring(pow(self.x // g, -1, self.ring.n) * (b // g))
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 permute_columns(self, permutation) -> None:
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 = False
385
- while not done:
386
- M = M.hermite_rnf(start = start, drop_zero_rows = drop_zero_rows)
387
- done = True
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 = False
441
+ done = min(j, done)
392
442
  pivot %= ring.n
393
- if pivot!=1 and gcd(pivot, ring.n) == 1: # we can make the pivot one
394
- done = False
395
- M.matrix[i][j] = 1
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 + 1, M.cols):
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
- def hermite_rnf(self, start = 0, drop_zero_rows: bool = False) -> "Matrix":
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 = [ self.matrix[i][:] for i in range(m) ]
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[i], H[i0] = H[i0], H[i] # swap rows, to move the pivot jn place
428
- if H[i][j] < 0:
429
- for k in range(n):
430
- H[i][k] *= -1 # make the pivot positive
431
- ii = i + 1
432
- while ii < m: # make the column entries below to the pivot zero
433
- tmp = H[ii][j] // H[i][j]
434
- for k in range(n):
435
- H[ii][k] -= tmp * H[i][k]
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
- tmp = H[ii][j] // H[i][j]
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 = Matrix(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
- raise NotImplementedError("The matrix must be over a field, not a ring.")
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 = Matrix(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
- else:
571
- ring = None
572
- for i in range(A.rows-1, -1, -1):
573
- j = A.pivotcols[i]
574
- b = A.matrix[i][-1]
575
- if ring:
576
- for k in range(j + 1, self.cols):
577
- b -= A.matrix[i][k] * solution[k]
578
- b = A.matrix[i][j].solve(b)
579
- if b is None:
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
- solution[j] = b
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 permute_columns(self, permutation) -> None:
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}.")