transcrypto 1.5.1__py3-none-any.whl → 1.7.0__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.
transcrypto/modmath.py CHANGED
@@ -1,7 +1,5 @@
1
- #!/usr/bin/env python3
2
- #
3
- # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
- #
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
5
3
  """Balparda's TransCrypto modular math library."""
6
4
 
7
5
  from __future__ import annotations
@@ -10,18 +8,12 @@ import concurrent.futures
10
8
  import math
11
9
  import multiprocessing
12
10
  import os
13
- # import pdb
14
- from typing import Generator, Reversible
11
+ from collections import abc
15
12
 
16
- import gmpy2 # type:ignore
13
+ import gmpy2
17
14
 
18
15
  from . import base, constants
19
16
 
20
- __author__ = 'balparda@github.com'
21
- __version__: str = base.__version__ # version comes from base!
22
- __version_tuple__: tuple[int, ...] = base.__version_tuple__
23
-
24
-
25
17
  _MAX_PRIMALITY_SAFETY = 100 # this is an absurd number, just to have a max
26
18
 
27
19
 
@@ -43,9 +35,10 @@ def ModInv(x: int, m: int, /) -> int:
43
35
  Raises:
44
36
  InputError: invalid modulus or x
45
37
  ModularDivideError: divide-by-zero, i.e., GCD(x, m) != 1 or x == 0
38
+
46
39
  """
47
40
  # test inputs
48
- if m < 2:
41
+ if m < 2: # noqa: PLR2004
49
42
  raise base.InputError(f'invalid modulus: {m=}')
50
43
  # easy special cases: 0 and 1
51
44
  reduced_x: int = x % m
@@ -57,7 +50,7 @@ def ModInv(x: int, m: int, /) -> int:
57
50
  gcd, y, w = base.ExtendedGCD(reduced_x, m)
58
51
  if gcd != 1:
59
52
  raise ModularDivideError(f'invalid inverse {x=} mod {m=} with {gcd=}')
60
- assert y and w and y >= -m, f'should never happen: {x=} mod {m=} -> {w=} ; {y=}'
53
+ assert y and w and y >= -m, f'should never happen: {x=} mod {m=} -> {w=} ; {y=}' # noqa: PT018, S101
61
54
  return y if y >= 0 else (y + m)
62
55
 
63
56
 
@@ -76,9 +69,10 @@ def ModDiv(x: int, y: int, m: int, /) -> int:
76
69
  Raises:
77
70
  InputError: invalid modulus or x or y
78
71
  ModularDivideError: divide-by-zero, i.e., GCD(y, m) != 1 or y == 0
72
+
79
73
  """
80
74
  # test inputs
81
- if m < 2:
75
+ if m < 2: # noqa: PLR2004
82
76
  raise base.InputError(f'invalid modulus: {m=}')
83
77
  if not y: # "division by 0"
84
78
  raise ModularDivideError(f'divide by zero {x=} / {y=} mod {m=}')
@@ -113,9 +107,10 @@ def CRTPair(a1: int, m1: int, a2: int, m2: int) -> int:
113
107
  Raises:
114
108
  InputError: invalid inputs
115
109
  ModularDivideError: moduli are not co-prime, i.e. gcd(m1, m2) != 1
110
+
116
111
  """
117
112
  # test inputs
118
- if m1 < 2 or m2 < 2 or m1 == m2:
113
+ if m1 < 2 or m2 < 2 or m1 == m2: # noqa: PLR2004
119
114
  raise base.InputError(f'invalid moduli: {m1=} / {m2=}')
120
115
  # compute
121
116
  a1 %= m1
@@ -143,9 +138,10 @@ def ModExp(x: int, y: int, m: int, /) -> int:
143
138
 
144
139
  Raises:
145
140
  InputError: invalid inputs
141
+
146
142
  """
147
143
  # test inputs
148
- if m < 2:
144
+ if m < 2: # noqa: PLR2004
149
145
  raise base.InputError(f'invalid modulus: {m=}')
150
146
  if y < 0:
151
147
  raise base.InputError(f'negative exponent: {y=}')
@@ -168,8 +164,8 @@ def ModExp(x: int, y: int, m: int, /) -> int:
168
164
  return z
169
165
 
170
166
 
171
- def ModPolynomial(x: int, polynomial: Reversible[int], m: int, /) -> int:
172
- """Evaluates `polynomial` (coefficients iterable) at `x` modulus `m`.
167
+ def ModPolynomial(x: int, polynomial: abc.Reversible[int], m: int, /) -> int:
168
+ """Evaluate `polynomial` (coefficients iterable) at `x` modulus `m`.
173
169
 
174
170
  Evaluate a polynomial at `x` under a modulus `m` using Horner's rule. Horner rewrites:
175
171
  a_0 + a_1 x + a_2 x^2 + … + a_n x^n
@@ -189,11 +185,12 @@ def ModPolynomial(x: int, polynomial: Reversible[int], m: int, /) -> int:
189
185
 
190
186
  Raises:
191
187
  InputError: invalid inputs
188
+
192
189
  """
193
190
  # test inputs
194
191
  if not polynomial:
195
192
  raise base.InputError(f'no polynomial: {polynomial=}')
196
- if m < 2:
193
+ if m < 2: # noqa: PLR2004
197
194
  raise base.InputError(f'invalid modulus: {m=}')
198
195
  # loop over polynomial coefficients
199
196
  total: int = 0
@@ -232,13 +229,14 @@ def ModLagrangeInterpolate(x: int, points: dict[int, int], m: int, /) -> int:
232
229
 
233
230
  Raises:
234
231
  InputError: invalid inputs
232
+
235
233
  """
236
234
  # test inputs
237
- if m < 2:
235
+ if m < 2: # noqa: PLR2004
238
236
  raise base.InputError(f'invalid modulus: {m=}')
239
237
  x %= m # takes care of negative numbers and also x >= m
240
238
  reduced_points: dict[int, int] = {k % m: v % m for k, v in points.items()}
241
- if len(points) < 2 or len(reduced_points) != len(points) or x in reduced_points:
239
+ if len(points) < 2 or len(reduced_points) != len(points) or x in reduced_points: # noqa: PLR2004
242
240
  raise base.InputError(f'invalid points or duplicate x/x_i found: {x=} / {points=}')
243
241
  # compute everything term-by-term
244
242
  result: int = 0
@@ -258,7 +256,7 @@ def ModLagrangeInterpolate(x: int, points: dict[int, int], m: int, /) -> int:
258
256
 
259
257
 
260
258
  def FermatIsPrime(n: int, /, *, safety: int = 10, witnesses: set[int] | None = None) -> bool:
261
- """Primality test of `n` by Fermat's algo (n > 0). DO NOT RELY!
259
+ """Primality test of `n` by Fermat's algo (n > 0) (UNRELIABLE!! -> use IsPrime()).
262
260
 
263
261
  Will execute Fermat's algo for non-trivial `n` (n > 3 and odd).
264
262
  <https://en.wikipedia.org/wiki/Fermat_primality_test>
@@ -278,11 +276,12 @@ def FermatIsPrime(n: int, /, *, safety: int = 10, witnesses: set[int] | None = N
278
276
 
279
277
  Raises:
280
278
  InputError: invalid inputs
279
+
281
280
  """
282
281
  # test inputs and test for trivial cases: 1, 2, 3, divisible by 2
283
282
  if n < 1:
284
283
  raise base.InputError(f'invalid number: {n=}')
285
- if n in (2, 3):
284
+ if n in {2, 3}:
286
285
  return True
287
286
  if n == 1 or not n % 2:
288
287
  return False
@@ -292,23 +291,23 @@ def FermatIsPrime(n: int, /, *, safety: int = 10, witnesses: set[int] | None = N
292
291
  max_safety: int = min(n // 2, _MAX_PRIMALITY_SAFETY)
293
292
  if safety < 1:
294
293
  raise base.InputError(f'out of bounds safety: 1 <= {safety=} <= {max_safety}')
295
- safety = max_safety if safety > max_safety else safety
294
+ safety = min(safety, max_safety)
296
295
  witnesses = set()
297
296
  while len(witnesses) < safety:
298
297
  witnesses.add(base.RandInt(2, n - 2))
299
298
  # we have our witnesses: do the actual Fermat algo
300
299
  for w in sorted(witnesses):
301
- if not 2 <= w <= (n - 2):
300
+ if not 2 <= w <= (n - 2): # noqa: PLR2004
302
301
  raise base.InputError(f'out of bounds witness: 2 ≤ {w=} ≤ {n - 2}')
303
- if gmpy2.powmod(w, n - 1, n) != 1: # type:ignore # pylint:disable=no-member
302
+ if gmpy2.powmod(w, n - 1, n) != 1:
304
303
  # number is proved to be composite
305
304
  return False
306
305
  # we declare the number PROBABLY a prime to the limits of this test
307
306
  return True
308
307
 
309
308
 
310
- def _MillerRabinWitnesses(n: int, /) -> set[int]: # pylint: disable=too-many-return-statements
311
- """Generates a reasonable set of Miller-Rabin witnesses for testing primality of `n`.
309
+ def _MillerRabinWitnesses(n: int, /) -> set[int]: # noqa: PLR0911
310
+ """Generate a reasonable set of Miller-Rabin witnesses for testing primality of `n`.
312
311
 
313
312
  For n < 3317044064679887385961981 it is precise. That is more than 2**81. See:
314
313
  <https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Testing_against_small_sets_of_bases>
@@ -325,35 +324,36 @@ def _MillerRabinWitnesses(n: int, /) -> set[int]: # pylint: disable=too-many-re
325
324
 
326
325
  Raises:
327
326
  InputError: invalid inputs
327
+
328
328
  """
329
329
  # test inputs
330
- if n < 5:
330
+ if n < 5: # noqa: PLR2004
331
331
  raise base.InputError(f'invalid number: {n=}')
332
332
  # for some "smaller" values there is research that shows these sets are always enough
333
- if n < 2047:
334
- return {2} # "safety" 1, but 100% coverage
335
- if n < 9080191:
336
- return {31, 73} # "safety" 2, but 100% coverage
337
- if n < 4759123141:
338
- return {2, 7, 61} # "safety" 3, but 100% coverage
339
- if n < 2152302898747:
340
- return set(constants.FIRST_5K_PRIMES_SORTED[:5]) # "safety" 5, but 100% coverage
341
- if n < 341550071728321:
342
- return set(constants.FIRST_5K_PRIMES_SORTED[:7]) # "safety" 7, but 100% coverage
343
- if n < 18446744073709551616: # 2 ** 64
333
+ if n < 2047: # noqa: PLR2004
334
+ return {2} # "safety" 1, but 100% coverage
335
+ if n < 9080191: # noqa: PLR2004
336
+ return {31, 73} # "safety" 2, but 100% coverage
337
+ if n < 4759123141: # noqa: PLR2004
338
+ return {2, 7, 61} # "safety" 3, but 100% coverage
339
+ if n < 2152302898747: # noqa: PLR2004
340
+ return set(constants.FIRST_5K_PRIMES_SORTED[:5]) # "safety" 5, but 100% coverage
341
+ if n < 341550071728321: # noqa: PLR2004
342
+ return set(constants.FIRST_5K_PRIMES_SORTED[:7]) # "safety" 7, but 100% coverage
343
+ if n < 18446744073709551616: # 2 ** 64 # noqa: PLR2004
344
344
  return set(constants.FIRST_5K_PRIMES_SORTED[:12]) # "safety" 12, but 100% coverage
345
- if n < 3317044064679887385961981: # > 2 ** 81
345
+ if n < 3317044064679887385961981: # > 2 ** 81 # noqa: PLR2004
346
346
  return set(constants.FIRST_5K_PRIMES_SORTED[:13]) # "safety" 13, but 100% coverage
347
347
  # here n should be greater than 2 ** 81, so safety should be 34 or less
348
348
  n_bits: int = n.bit_length()
349
- assert n_bits >= 82, f'should never happen: {n=} -> {n_bits=}'
350
- safety: int = int(math.ceil(0.375 + 1.59 / (0.000590 * n_bits))) if n_bits <= 1700 else 2
351
- assert 1 < safety <= 34, f'should never happen: {n=} -> {n_bits=} ; {safety=}'
349
+ assert n_bits >= 82, f'should never happen: {n=} -> {n_bits=}' # noqa: PLR2004, S101
350
+ safety: int = max(2, math.ceil(0.375 + 1.59 / (0.000590 * n_bits))) if n_bits <= 1700 else 2 # noqa: PLR2004
351
+ assert 1 < safety <= 34, f'should never happen: {n=} -> {n_bits=} ; {safety=}' # noqa: PLR2004, S101
352
352
  return set(constants.FIRST_5K_PRIMES_SORTED[:safety])
353
353
 
354
354
 
355
355
  def _MillerRabinSR(n: int, /) -> tuple[int, int]:
356
- """Generates (s, r) where (2 ** s) * r == (n - 1) hold true, for odd n > 5.
356
+ """Generate (s, r) where (2 ** s) * r == (n - 1) hold true, for odd n > 5.
357
357
 
358
358
  It should be always true that: s ≥ 1 and r ≥ 1 and r is odd.
359
359
 
@@ -365,9 +365,10 @@ def _MillerRabinSR(n: int, /) -> tuple[int, int]:
365
365
 
366
366
  Raises:
367
367
  InputError: invalid inputs
368
+
368
369
  """
369
370
  # test inputs
370
- if n < 5 or not n % 2:
371
+ if n < 5 or not n % 2: # noqa: PLR2004
371
372
  raise base.InputError(f'invalid odd number: {n=}')
372
373
  # divide by 2 until we can't anymore
373
374
  s: int = 1
@@ -376,7 +377,7 @@ def _MillerRabinSR(n: int, /) -> tuple[int, int]:
376
377
  s += 1
377
378
  r //= 2
378
379
  # make sure everything checks out and return
379
- assert 1 <= r <= n and r % 2, f'should never happen: {n=} -> {r=}'
380
+ assert 1 <= r <= n and r % 2, f'should never happen: {n=} -> {r=}' # noqa: PT018, S101
380
381
  return (s, r)
381
382
 
382
383
 
@@ -395,11 +396,12 @@ def MillerRabinIsPrime(n: int, /, *, witnesses: set[int] | None = None) -> bool:
395
396
 
396
397
  Raises:
397
398
  InputError: invalid inputs
399
+
398
400
  """
399
401
  # test inputs and test for trivial cases: 1, 2, 3, divisible by 2
400
402
  if n < 1:
401
403
  raise base.InputError(f'invalid number: {n=}')
402
- if n in (2, 3):
404
+ if n in {2, 3}:
403
405
  return True
404
406
  if n == 1 or not n % 2:
405
407
  return False
@@ -408,10 +410,10 @@ def MillerRabinIsPrime(n: int, /, *, witnesses: set[int] | None = None) -> bool:
408
410
  # do the Miller-Rabin algo
409
411
  n_limits: tuple[int, int] = (1, n - 1)
410
412
  y: int
411
- for w in sorted(witnesses if witnesses else _MillerRabinWitnesses(n)):
412
- if not 2 <= w <= (n - 2):
413
+ for w in sorted(witnesses or _MillerRabinWitnesses(n)):
414
+ if not 2 <= w <= (n - 2): # noqa: PLR2004
413
415
  raise base.InputError(f'out of bounds witness: 2 ≤ {w=} ≤ {n - 2}')
414
- x: int = int(gmpy2.powmod(w, r, n)) # type:ignore # pylint:disable=no-member
416
+ x: int = int(gmpy2.powmod(w, r, n))
415
417
  if x not in n_limits:
416
418
  for _ in range(s): # s >= 1 so will execute at least once
417
419
  y = (x * x) % n
@@ -419,7 +421,7 @@ def MillerRabinIsPrime(n: int, /, *, witnesses: set[int] | None = None) -> bool:
419
421
  return False # number is proved to be composite
420
422
  x = y
421
423
  if x != 1:
422
- return False # number is proved to be composite
424
+ return False # number is proved to be composite
423
425
  # we declare the number PROBABLY a prime to the limits of this test
424
426
  return True
425
427
 
@@ -433,8 +435,6 @@ def IsPrime(n: int, /) -> bool:
433
435
  Returns:
434
436
  False if certainly not prime ; True if (probabilistically) prime
435
437
 
436
- Raises:
437
- InputError: invalid inputs
438
438
  """
439
439
  # is number divisible by (one of the) first 20000 primes? test should eliminate 90%+ of candidates
440
440
  if n in constants.FIRST_20K_PRIMES:
@@ -446,8 +446,8 @@ def IsPrime(n: int, /) -> bool:
446
446
  return MillerRabinIsPrime(n)
447
447
 
448
448
 
449
- def PrimeGenerator(start: int, /) -> Generator[int, None, None]:
450
- """Generates all primes from `start` until loop is broken. Tuned for huge numbers.
449
+ def PrimeGenerator(start: int, /) -> abc.Generator[int]:
450
+ """Generate all primes from `start` until loop is broken. Tuned for huge numbers.
451
451
 
452
452
  Args:
453
453
  start (int): number at which to start generating primes, start ≥ 0
@@ -457,12 +457,13 @@ def PrimeGenerator(start: int, /) -> Generator[int, None, None]:
457
457
 
458
458
  Raises:
459
459
  InputError: invalid inputs
460
+
460
461
  """
461
462
  # test inputs and make sure we start at an odd number
462
463
  if start < 0:
463
464
  raise base.InputError(f'negative number: {start=}')
464
465
  # handle start of sequence manually if needed... because we have here the only EVEN prime...
465
- if start <= 2:
466
+ if start <= 2: # noqa: PLR2004
466
467
  yield 2
467
468
  start = 3
468
469
  # we now focus on odd numbers only and loop forever
@@ -474,7 +475,7 @@ def PrimeGenerator(start: int, /) -> Generator[int, None, None]:
474
475
 
475
476
 
476
477
  def NBitRandomPrimes(n_bits: int, /, *, serial: bool = True, n_primes: int = 1) -> set[int]:
477
- """Generates a random prime with (guaranteed) `n_bits` size (i.e., first bit == 1).
478
+ """Generate a random prime with (guaranteed) `n_bits` size (i.e., first bit == 1).
478
479
 
479
480
  The fact that the first bit will be 1 means the entropy is ~ (n_bits-1) and
480
481
  because of this we only allow for a byte or more prime bits generated. This drawback
@@ -508,16 +509,18 @@ def NBitRandomPrimes(n_bits: int, /, *, serial: bool = True, n_primes: int = 1)
508
509
 
509
510
  Raises:
510
511
  InputError: invalid inputs
512
+ Error: prime search failed
513
+
511
514
  """
512
515
  # test inputs
513
- if n_bits < 8:
516
+ if n_bits < 8: # noqa: PLR2004
514
517
  raise base.InputError(f'invalid n: {n_bits=}')
515
- n_primes = 1 if n_primes < 1 else n_primes
518
+ n_primes = max(n_primes, 1)
516
519
  # get number of CPUs and decide if we do parallel or not
517
520
  n_workers: int = min(4, os.cpu_count() or 1)
518
521
  pr_set: set[int] = set()
519
522
  pr: int | None = None
520
- if serial or n_workers <= 1 or n_bits < 200:
523
+ if serial or n_workers <= 1 or n_bits < 200: # noqa: PLR2004
521
524
  # do one worker
522
525
  while len(pr_set) < n_primes:
523
526
  while pr is None or pr.bit_length() != n_bits:
@@ -529,10 +532,12 @@ def NBitRandomPrimes(n_bits: int, /, *, serial: bool = True, n_primes: int = 1)
529
532
  multiprocessing.set_start_method('fork', force=True)
530
533
  with concurrent.futures.ProcessPoolExecutor(max_workers=n_workers) as pool:
531
534
  workers: set[concurrent.futures.Future[int | None]] = {
532
- pool.submit(_PrimeSearchShard, n_bits) for _ in range(n_workers)}
535
+ pool.submit(_PrimeSearchShard, n_bits) for _ in range(n_workers)
536
+ }
533
537
  while workers:
534
538
  done: set[concurrent.futures.Future[int | None]] = concurrent.futures.wait(
535
- workers, return_when=concurrent.futures.FIRST_COMPLETED)[0]
539
+ workers, return_when=concurrent.futures.FIRST_COMPLETED
540
+ )[0]
536
541
  for worker in done:
537
542
  workers.remove(worker)
538
543
  pr = worker.result()
@@ -548,15 +553,16 @@ def NBitRandomPrimes(n_bits: int, /, *, serial: bool = True, n_primes: int = 1)
548
553
 
549
554
 
550
555
  def _PrimeSearchShard(n_bits: int) -> int | None:
551
- """Search for a `n_bits` random prime, starting from a random point, for ~ expected prime gap.
556
+ """Search for a `n_bits` random prime, starting from a random point, for ~6x expected prime gap.
552
557
 
553
558
  Args:
554
559
  n_bits (int): Number of guaranteed bits in prime representation
555
560
 
556
561
  Returns:
557
562
  int | None: either the prime int or None if no prime found in this shard
563
+
558
564
  """
559
- shard_len: int = max(2000, 6 * int(0.693 * n_bits)) # ~ expected prime gap ~2^k (≈ 0.693*k)
565
+ shard_len: int = max(2000, 6 * int(0.693 * n_bits)) # ~6x expected prime gap ~2^k (≈ 0.693*k)
560
566
  pr: int = base.RandBits(n_bits) | 1 # random position; make ODD
561
567
  count: int = 0
562
568
  while count < shard_len and pr.bit_length() == n_bits:
@@ -567,18 +573,24 @@ def _PrimeSearchShard(n_bits: int) -> int | None:
567
573
  return None
568
574
 
569
575
 
570
- def FirstNPrimesSorted(n: int) -> list[int]:
571
- """Returns list of `n` first primes in a sorted list."""
572
- primes: list[int] = []
576
+ def FirstNPrimesSorted(n: int) -> abc.Generator[int]:
577
+ """Return list of `n` first primes in a sorted list.
578
+
579
+ Args:
580
+ n (int): number of primes to return
581
+
582
+ Yields:
583
+ Generator[int]: primes
584
+
585
+ """
573
586
  for i, pr in enumerate(PrimeGenerator(0)):
574
587
  if i >= n:
575
- break
576
- primes.append(pr)
577
- return primes
588
+ return
589
+ yield pr
578
590
 
579
591
 
580
- def MersennePrimesGenerator(start: int, /) -> Generator[tuple[int, int, int], None, None]:
581
- """Generates all Mersenne prime (2 ** n - 1) exponents from 2**start until loop is broken.
592
+ def MersennePrimesGenerator(start: int, /) -> abc.Generator[tuple[int, int, int]]:
593
+ """Generate all Mersenne prime (2 ** n - 1) exponents from start until loop is broken.
582
594
 
583
595
  <https://en.wikipedia.org/wiki/List_of_Mersenne_primes_and_perfect_numbers>
584
596
 
@@ -589,12 +601,10 @@ def MersennePrimesGenerator(start: int, /) -> Generator[tuple[int, int, int], No
589
601
  (exponent, mersenne_prime, perfect_number), given some exponent `n` that will be exactly:
590
602
  (n, 2 ** n - 1, (2 ** (n - 1)) * (2 ** n - 1))
591
603
 
592
- Raises:
593
- InputError: invalid inputs
594
604
  """
595
605
  # we now loop forever over prime exponents
596
606
  # "The exponents p corresponding to Mersenne primes must themselves be prime."
597
- for n in PrimeGenerator(start if start >= 1 else 1):
598
- mersenne: int = 2 ** n - 1
607
+ for n in PrimeGenerator(max(start, 1)):
608
+ mersenne: int = 2**n - 1
599
609
  if IsPrime(mersenne):
600
610
  yield (n, mersenne, (2 ** (n - 1)) * mersenne) # found: also yield perfect number