foxmath 0.6.2__tar.gz → 0.7.1__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.
- {foxmath-0.6.2/src/foxmath.egg-info → foxmath-0.7.1}/PKG-INFO +16 -4
- {foxmath-0.6.2 → foxmath-0.7.1}/README.md +15 -3
- {foxmath-0.6.2 → foxmath-0.7.1}/pyproject.toml +2 -1
- {foxmath-0.6.2 → foxmath-0.7.1}/src/foxmath/foxmath.py +121 -3
- {foxmath-0.6.2 → foxmath-0.7.1/src/foxmath.egg-info}/PKG-INFO +16 -4
- {foxmath-0.6.2 → foxmath-0.7.1}/src/foxmath.egg-info/entry_points.txt +1 -0
- {foxmath-0.6.2 → foxmath-0.7.1}/tests/test_cli.py +37 -1
- {foxmath-0.6.2 → foxmath-0.7.1}/tests/test_foxmath.py +76 -2
- {foxmath-0.6.2 → foxmath-0.7.1}/LICENSE +0 -0
- {foxmath-0.6.2 → foxmath-0.7.1}/setup.cfg +0 -0
- {foxmath-0.6.2 → foxmath-0.7.1}/src/foxmath/__init__.py +0 -0
- {foxmath-0.6.2 → foxmath-0.7.1}/src/foxmath.egg-info/SOURCES.txt +0 -0
- {foxmath-0.6.2 → foxmath-0.7.1}/src/foxmath.egg-info/dependency_links.txt +0 -0
- {foxmath-0.6.2 → foxmath-0.7.1}/src/foxmath.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: foxmath
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Minimalist CLI for math & cryptography exploration 🦊
|
|
5
5
|
Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,7 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
- Legendre symbol
|
|
32
|
+
- Modular square root via Tonelli-Shanks (sqrtmod)
|
|
32
33
|
- Euler/Hermann Machin-like π approximation
|
|
33
34
|
- Elliptic curve point addition (over finite fields), including
|
|
34
35
|
named curves (secp256k1) with on-curve result validation
|
|
@@ -38,8 +39,8 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
|
|
|
38
39
|
- Continued fraction expansion and convergents
|
|
39
40
|
- Challenge/quiz mode — practice problems with scoring, or a
|
|
40
41
|
worksheet mode for self-study
|
|
41
|
-
- Symlink-friendly (foxmath-legendre, foxmath-
|
|
42
|
-
foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
42
|
+
- Symlink-friendly (foxmath-legendre, foxmath-sqrtmod, foxmath-pi,
|
|
43
|
+
foxmath-ecadd, foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
43
44
|
- JSON output mode
|
|
44
45
|
|
|
45
46
|
## Installation
|
|
@@ -60,7 +61,7 @@ Check which version you have installed:
|
|
|
60
61
|
foxmath --version
|
|
61
62
|
```
|
|
62
63
|
```
|
|
63
|
-
foxmath 0.
|
|
64
|
+
foxmath 0.7.1
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
## Usage Examples
|
|
@@ -79,6 +80,17 @@ foxmath legendre 5 17
|
|
|
79
80
|
Legendre (5/17) = -1
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
```bash
|
|
84
|
+
foxmath sqrtmod 10 13
|
|
85
|
+
```
|
|
86
|
+
```
|
|
87
|
+
sqrt(10) = +/-7 (mod 13)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`sqrtmod` uses the Tonelli-Shanks algorithm to find modular square roots
|
|
91
|
+
when they exist. It reports an error when the input is a quadratic
|
|
92
|
+
non-residue modulo the given odd prime.
|
|
93
|
+
|
|
82
94
|
```bash
|
|
83
95
|
# Symlinks (optional)
|
|
84
96
|
foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
|
|
@@ -10,6 +10,7 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
- Legendre symbol
|
|
13
|
+
- Modular square root via Tonelli-Shanks (sqrtmod)
|
|
13
14
|
- Euler/Hermann Machin-like π approximation
|
|
14
15
|
- Elliptic curve point addition (over finite fields), including
|
|
15
16
|
named curves (secp256k1) with on-curve result validation
|
|
@@ -19,8 +20,8 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
|
|
|
19
20
|
- Continued fraction expansion and convergents
|
|
20
21
|
- Challenge/quiz mode — practice problems with scoring, or a
|
|
21
22
|
worksheet mode for self-study
|
|
22
|
-
- Symlink-friendly (foxmath-legendre, foxmath-
|
|
23
|
-
foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
23
|
+
- Symlink-friendly (foxmath-legendre, foxmath-sqrtmod, foxmath-pi,
|
|
24
|
+
foxmath-ecadd, foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
24
25
|
- JSON output mode
|
|
25
26
|
|
|
26
27
|
## Installation
|
|
@@ -41,7 +42,7 @@ Check which version you have installed:
|
|
|
41
42
|
foxmath --version
|
|
42
43
|
```
|
|
43
44
|
```
|
|
44
|
-
foxmath 0.
|
|
45
|
+
foxmath 0.7.1
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
## Usage Examples
|
|
@@ -60,6 +61,17 @@ foxmath legendre 5 17
|
|
|
60
61
|
Legendre (5/17) = -1
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
```bash
|
|
65
|
+
foxmath sqrtmod 10 13
|
|
66
|
+
```
|
|
67
|
+
```
|
|
68
|
+
sqrt(10) = +/-7 (mod 13)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`sqrtmod` uses the Tonelli-Shanks algorithm to find modular square roots
|
|
72
|
+
when they exist. It reports an error when the input is a quadratic
|
|
73
|
+
non-residue modulo the given odd prime.
|
|
74
|
+
|
|
63
75
|
```bash
|
|
64
76
|
# Symlinks (optional)
|
|
65
77
|
foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "foxmath"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.1"
|
|
8
8
|
description = "Minimalist CLI for math & cryptography exploration 🦊"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -29,6 +29,7 @@ dependencies = [] # Pure stdlib for now
|
|
|
29
29
|
[project.scripts]
|
|
30
30
|
foxmath = "foxmath.foxmath:main"
|
|
31
31
|
foxmath-legendre = "foxmath.foxmath:main"
|
|
32
|
+
foxmath-sqrtmod = "foxmath.foxmath:main"
|
|
32
33
|
foxmath-pi = "foxmath.foxmath:main"
|
|
33
34
|
foxmath-ecadd = "foxmath.foxmath:main"
|
|
34
35
|
foxmath-ecmul = "foxmath.foxmath:main"
|
|
@@ -22,13 +22,107 @@ def _package_version() -> str:
|
|
|
22
22
|
|
|
23
23
|
# ------------------- Math Functions -------------------
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
# First 13 primes. As Miller-Rabin bases they make the test *deterministic*
|
|
26
|
+
# for every n below 3,317,044,064,679,887,385,961,981 (~3.3e24); 12 bases
|
|
27
|
+
# would not be enough (399165290221 * 798330580441, ~3.2e23, fools them).
|
|
28
|
+
_PRIME_TEST_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
|
|
29
|
+
|
|
30
|
+
def _is_probable_prime(n: int) -> bool:
|
|
31
|
+
"""Miller-Rabin primality test.
|
|
32
|
+
|
|
33
|
+
Exact for n < ~3.3e24. Above that it is a strong probable-prime test:
|
|
34
|
+
ordinary composites essentially never pass, but a composite built
|
|
35
|
+
specifically to fool these 13 bases could.
|
|
36
|
+
"""
|
|
37
|
+
if n < 2:
|
|
38
|
+
return False
|
|
39
|
+
for sp in _PRIME_TEST_BASES:
|
|
40
|
+
if n == sp:
|
|
41
|
+
return True
|
|
42
|
+
if n % sp == 0:
|
|
43
|
+
return False
|
|
44
|
+
d, r = n - 1, 0
|
|
45
|
+
while d % 2 == 0:
|
|
46
|
+
d //= 2
|
|
47
|
+
r += 1
|
|
48
|
+
for a in _PRIME_TEST_BASES:
|
|
49
|
+
x = pow(a, d, n)
|
|
50
|
+
if x == 1 or x == n - 1:
|
|
51
|
+
continue
|
|
52
|
+
for _ in range(r - 1):
|
|
53
|
+
x = x * x % n
|
|
54
|
+
if x == n - 1:
|
|
55
|
+
break
|
|
56
|
+
else:
|
|
57
|
+
return False
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def _require_odd_prime(p: int) -> None:
|
|
61
|
+
"""Enforce the contract the Legendre symbol and Tonelli-Shanks rely on.
|
|
62
|
+
|
|
63
|
+
Euler's criterion is only meaningful for a prime modulus. On a composite
|
|
64
|
+
it returns garbage, and Tonelli-Shanks' search for a non-residue can loop
|
|
65
|
+
forever (mod 9 or mod 561, for example, there is none to find).
|
|
66
|
+
"""
|
|
67
|
+
if p < 2 or p % 2 == 0 or not _is_probable_prime(p):
|
|
28
68
|
raise ValueError("p must be an odd prime")
|
|
69
|
+
|
|
70
|
+
def _legendre_unchecked(a: int, p: int) -> int:
|
|
29
71
|
res = pow(a, (p - 1) // 2, p)
|
|
30
72
|
return 1 if res == 1 else (-1 if res == p - 1 else 0)
|
|
31
73
|
|
|
74
|
+
def legendre_symbol(a: int, p: int) -> int:
|
|
75
|
+
"""Compute Legendre symbol (a/p). p must be an odd prime."""
|
|
76
|
+
_require_odd_prime(p)
|
|
77
|
+
return _legendre_unchecked(a, p)
|
|
78
|
+
|
|
79
|
+
def sqrt_mod(a: int, p: int) -> tuple:
|
|
80
|
+
"""Return the square root(s) of a modulo an odd prime p."""
|
|
81
|
+
_require_odd_prime(p)
|
|
82
|
+
|
|
83
|
+
a %= p
|
|
84
|
+
symbol = _legendre_unchecked(a, p)
|
|
85
|
+
if symbol == 0:
|
|
86
|
+
return (0,)
|
|
87
|
+
if symbol == -1:
|
|
88
|
+
raise ValueError(f"{a} has no square root modulo {p}")
|
|
89
|
+
|
|
90
|
+
# For p ≡ 3 (mod 4), the root is a^(p+1)/4.
|
|
91
|
+
if p % 4 == 3:
|
|
92
|
+
root = pow(a, (p + 1) // 4, p)
|
|
93
|
+
else:
|
|
94
|
+
# Tonelli-Shanks: write p - 1 = q * 2^s with q odd.
|
|
95
|
+
q, s = p - 1, 0
|
|
96
|
+
while q % 2 == 0:
|
|
97
|
+
q //= 2
|
|
98
|
+
s += 1
|
|
99
|
+
|
|
100
|
+
non_residue = 2
|
|
101
|
+
while _legendre_unchecked(non_residue, p) != -1:
|
|
102
|
+
non_residue += 1
|
|
103
|
+
|
|
104
|
+
c = pow(non_residue, q, p)
|
|
105
|
+
root = pow(a, (q + 1) // 2, p)
|
|
106
|
+
t = pow(a, q, p)
|
|
107
|
+
m = s
|
|
108
|
+
|
|
109
|
+
while t != 1:
|
|
110
|
+
i = 1
|
|
111
|
+
t_squared = (t * t) % p
|
|
112
|
+
while t_squared != 1:
|
|
113
|
+
t_squared = (t_squared * t_squared) % p
|
|
114
|
+
i += 1
|
|
115
|
+
if i == m:
|
|
116
|
+
raise ValueError(f"{a} has no square root modulo {p}")
|
|
117
|
+
b = pow(c, 1 << (m - i - 1), p)
|
|
118
|
+
root = (root * b) % p
|
|
119
|
+
t = (t * b * b) % p
|
|
120
|
+
c = (b * b) % p
|
|
121
|
+
m = i
|
|
122
|
+
|
|
123
|
+
other = (-root) % p
|
|
124
|
+
return tuple(sorted({root, other}))
|
|
125
|
+
|
|
32
126
|
def _arctan_series(inv_n: Decimal, terms: int) -> Decimal:
|
|
33
127
|
"""arctan(1/n) via its Taylor series, evaluated to `terms` terms. `inv_n` is 1/n."""
|
|
34
128
|
total = Decimal(0)
|
|
@@ -283,6 +377,17 @@ def get_command():
|
|
|
283
377
|
return None
|
|
284
378
|
|
|
285
379
|
def main():
|
|
380
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
381
|
+
try:
|
|
382
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
386
|
+
try:
|
|
387
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
388
|
+
except Exception:
|
|
389
|
+
pass
|
|
390
|
+
|
|
286
391
|
cmd = get_command()
|
|
287
392
|
|
|
288
393
|
# Handle --json independently of argparse subparser scoping: a subparser's
|
|
@@ -311,6 +416,10 @@ def main():
|
|
|
311
416
|
leg.add_argument("a", type=int, help="Integer a")
|
|
312
417
|
leg.add_argument("p", type=int, help="Odd prime p")
|
|
313
418
|
|
|
419
|
+
sqrt = subparsers.add_parser("sqrtmod", help="Square root(s) modulo an odd prime")
|
|
420
|
+
sqrt.add_argument("a", type=int, help="Integer a")
|
|
421
|
+
sqrt.add_argument("p", type=int, help="Odd prime p")
|
|
422
|
+
|
|
314
423
|
# Pi
|
|
315
424
|
pi_cmd = subparsers.add_parser("pi", help="Euler/Hermann Machin-like π approximation")
|
|
316
425
|
pi_cmd.add_argument("--terms", type=int, default=100)
|
|
@@ -366,6 +475,15 @@ def main():
|
|
|
366
475
|
if not json_output:
|
|
367
476
|
print(f"Legendre ({args.a}/{args.p}) = {ls}")
|
|
368
477
|
|
|
478
|
+
elif cmd == "sqrtmod" or args.command == "sqrtmod":
|
|
479
|
+
roots = sqrt_mod(args.a, args.p)
|
|
480
|
+
result.update({"command": "sqrtmod", "a": args.a, "p": args.p, "roots": list(roots)})
|
|
481
|
+
if not json_output:
|
|
482
|
+
if len(roots) == 1:
|
|
483
|
+
print(f"sqrt({args.a}) = {roots[0]} (mod {args.p})")
|
|
484
|
+
else:
|
|
485
|
+
print(f"sqrt({args.a}) = +/-{roots[1]} (mod {args.p})")
|
|
486
|
+
|
|
369
487
|
elif cmd == "pi" or args.command == "pi":
|
|
370
488
|
approx = euler_hermann_pi_approx(args.terms)
|
|
371
489
|
result.update({"command": "pi", "terms": args.terms, "approx": str(approx)})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: foxmath
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Minimalist CLI for math & cryptography exploration 🦊
|
|
5
5
|
Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,7 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
- Legendre symbol
|
|
32
|
+
- Modular square root via Tonelli-Shanks (sqrtmod)
|
|
32
33
|
- Euler/Hermann Machin-like π approximation
|
|
33
34
|
- Elliptic curve point addition (over finite fields), including
|
|
34
35
|
named curves (secp256k1) with on-curve result validation
|
|
@@ -38,8 +39,8 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
|
|
|
38
39
|
- Continued fraction expansion and convergents
|
|
39
40
|
- Challenge/quiz mode — practice problems with scoring, or a
|
|
40
41
|
worksheet mode for self-study
|
|
41
|
-
- Symlink-friendly (foxmath-legendre, foxmath-
|
|
42
|
-
foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
42
|
+
- Symlink-friendly (foxmath-legendre, foxmath-sqrtmod, foxmath-pi,
|
|
43
|
+
foxmath-ecadd, foxmath-ecmul, foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
43
44
|
- JSON output mode
|
|
44
45
|
|
|
45
46
|
## Installation
|
|
@@ -60,7 +61,7 @@ Check which version you have installed:
|
|
|
60
61
|
foxmath --version
|
|
61
62
|
```
|
|
62
63
|
```
|
|
63
|
-
foxmath 0.
|
|
64
|
+
foxmath 0.7.1
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
## Usage Examples
|
|
@@ -79,6 +80,17 @@ foxmath legendre 5 17
|
|
|
79
80
|
Legendre (5/17) = -1
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
```bash
|
|
84
|
+
foxmath sqrtmod 10 13
|
|
85
|
+
```
|
|
86
|
+
```
|
|
87
|
+
sqrt(10) = +/-7 (mod 13)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`sqrtmod` uses the Tonelli-Shanks algorithm to find modular square roots
|
|
91
|
+
when they exist. It reports an error when the input is a quadratic
|
|
92
|
+
non-residue modulo the given odd prime.
|
|
93
|
+
|
|
82
94
|
```bash
|
|
83
95
|
# Symlinks (optional)
|
|
84
96
|
foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
|
|
@@ -16,7 +16,7 @@ SCRIPT = os.path.join(ROOT, "foxmath.py")
|
|
|
16
16
|
def run(*args, script=SCRIPT, stdin_input=None):
|
|
17
17
|
result = subprocess.run(
|
|
18
18
|
[sys.executable, script, *args],
|
|
19
|
-
capture_output=True, text=True, timeout=15, input=stdin_input,
|
|
19
|
+
capture_output=True, text=True, encoding="utf-8", timeout=15, input=stdin_input,
|
|
20
20
|
)
|
|
21
21
|
return result.returncode, result.stdout, result.stderr
|
|
22
22
|
|
|
@@ -38,6 +38,16 @@ class TestJsonFlag(unittest.TestCase):
|
|
|
38
38
|
self.assertEqual(code, 0)
|
|
39
39
|
self.assertNotIn("{", out)
|
|
40
40
|
|
|
41
|
+
def test_sqrtmod(self):
|
|
42
|
+
code, out, err = run("sqrtmod", "10", "13")
|
|
43
|
+
self.assertEqual(code, 0)
|
|
44
|
+
self.assertIn("sqrt(10) = +/-7 (mod 13)", out)
|
|
45
|
+
|
|
46
|
+
def test_sqrtmod_json(self):
|
|
47
|
+
code, out, err = run("sqrtmod", "10", "13", "--json")
|
|
48
|
+
self.assertEqual(code, 0)
|
|
49
|
+
self.assertIn('"roots": [', out)
|
|
50
|
+
|
|
41
51
|
def test_json_output_is_pure_json_no_preamble(self):
|
|
42
52
|
# Regression check: --json used to still print the human-readable
|
|
43
53
|
# line first, breaking anything piping stdout into a JSON parser.
|
|
@@ -66,6 +76,12 @@ class TestSymlinkInvocation(unittest.TestCase):
|
|
|
66
76
|
self.assertEqual(code, 0)
|
|
67
77
|
self.assertIn("Legendre (2/7) = 1", out)
|
|
68
78
|
|
|
79
|
+
def test_sqrtmod_symlink(self):
|
|
80
|
+
script = self._make_symlink_script("foxmath-sqrtmod")
|
|
81
|
+
code, out, err = run("10", "13", script=script)
|
|
82
|
+
self.assertEqual(code, 0)
|
|
83
|
+
self.assertIn("sqrt(10) = +/-7 (mod 13)", out)
|
|
84
|
+
|
|
69
85
|
def test_pi_symlink(self):
|
|
70
86
|
script = self._make_symlink_script("foxmath-pi")
|
|
71
87
|
code, out, err = run("--terms", "10", script=script)
|
|
@@ -275,6 +291,26 @@ class TestNormalUsage(unittest.TestCase):
|
|
|
275
291
|
self.assertEqual(code, 1)
|
|
276
292
|
self.assertIn("Error", err)
|
|
277
293
|
|
|
294
|
+
def test_sqrtmod_non_residue_errors(self):
|
|
295
|
+
code, out, err = run("sqrtmod", "5", "17")
|
|
296
|
+
self.assertEqual(code, 1)
|
|
297
|
+
self.assertIn("no square root", err)
|
|
298
|
+
|
|
299
|
+
def test_sqrtmod_zero(self):
|
|
300
|
+
code, out, err = run("sqrtmod", "0", "17")
|
|
301
|
+
self.assertEqual(code, 0)
|
|
302
|
+
self.assertIn("sqrt(0) = 0 (mod 17)", out)
|
|
303
|
+
|
|
304
|
+
def test_legendre_composite_p_errors(self):
|
|
305
|
+
code, out, err = run("legendre", "2", "9")
|
|
306
|
+
self.assertEqual(code, 1)
|
|
307
|
+
self.assertIn("p must be an odd prime", err)
|
|
308
|
+
|
|
309
|
+
def test_sqrtmod_composite_p_errors(self):
|
|
310
|
+
code, out, err = run("sqrtmod", "2", "9")
|
|
311
|
+
self.assertEqual(code, 1)
|
|
312
|
+
self.assertIn("p must be an odd prime", err)
|
|
313
|
+
|
|
278
314
|
|
|
279
315
|
if __name__ == "__main__":
|
|
280
316
|
unittest.main()
|
|
@@ -13,9 +13,9 @@ from decimal import Decimal
|
|
|
13
13
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
|
14
14
|
|
|
15
15
|
from foxmath.foxmath import (
|
|
16
|
-
legendre_symbol, euler_hermann_pi_approx, ec_point_add, ec_mul, crt_solve,
|
|
16
|
+
legendre_symbol, sqrt_mod, euler_hermann_pi_approx, ec_point_add, ec_mul, crt_solve,
|
|
17
17
|
continued_fraction, cf_convergents, CURVES, is_on_curve,
|
|
18
|
-
generate_problem,
|
|
18
|
+
generate_problem, _is_probable_prime,
|
|
19
19
|
)
|
|
20
20
|
import random
|
|
21
21
|
|
|
@@ -51,6 +51,80 @@ class TestLegendreSymbol(unittest.TestCase):
|
|
|
51
51
|
with self.assertRaises(ValueError):
|
|
52
52
|
legendre_symbol(3, 1)
|
|
53
53
|
|
|
54
|
+
def test_rejects_composite_odd_p(self):
|
|
55
|
+
for comp in (9, 15, 21, 25, 27, 33, 35, 561):
|
|
56
|
+
with self.subTest(p=comp):
|
|
57
|
+
with self.assertRaises(ValueError):
|
|
58
|
+
legendre_symbol(2, comp)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestSqrtMod(unittest.TestCase):
|
|
62
|
+
def test_tonelli_shanks_returns_both_roots(self):
|
|
63
|
+
roots = sqrt_mod(10, 13)
|
|
64
|
+
self.assertEqual(roots, (6, 7))
|
|
65
|
+
self.assertTrue(all((root * root - 10) % 13 == 0 for root in roots))
|
|
66
|
+
|
|
67
|
+
def test_fast_path_for_prime_three_mod_four(self):
|
|
68
|
+
self.assertEqual(sqrt_mod(9, 11), (3, 8))
|
|
69
|
+
|
|
70
|
+
def test_zero_has_one_root(self):
|
|
71
|
+
self.assertEqual(sqrt_mod(0, 17), (0,))
|
|
72
|
+
|
|
73
|
+
def test_rejects_non_residue(self):
|
|
74
|
+
with self.assertRaises(ValueError):
|
|
75
|
+
sqrt_mod(5, 17)
|
|
76
|
+
|
|
77
|
+
def test_rejects_even_p(self):
|
|
78
|
+
with self.assertRaises(ValueError):
|
|
79
|
+
sqrt_mod(1, 2)
|
|
80
|
+
|
|
81
|
+
def test_rejects_p_less_than_2(self):
|
|
82
|
+
with self.assertRaises(ValueError):
|
|
83
|
+
sqrt_mod(1, 1)
|
|
84
|
+
with self.assertRaises(ValueError):
|
|
85
|
+
sqrt_mod(1, -5)
|
|
86
|
+
|
|
87
|
+
def test_normalizes_a_greater_than_p(self):
|
|
88
|
+
self.assertEqual(sqrt_mod(10 + 26, 13), (6, 7))
|
|
89
|
+
|
|
90
|
+
def test_negative_a(self):
|
|
91
|
+
self.assertEqual(sqrt_mod(-3, 13), (6, 7))
|
|
92
|
+
|
|
93
|
+
def test_larger_primes_tonelli_shanks(self):
|
|
94
|
+
roots_41 = sqrt_mod(8, 41)
|
|
95
|
+
self.assertEqual(roots_41, (7, 34))
|
|
96
|
+
self.assertTrue(all((r * r - 8) % 41 == 0 for r in roots_41))
|
|
97
|
+
|
|
98
|
+
roots_97 = sqrt_mod(2, 97)
|
|
99
|
+
self.assertEqual(roots_97, (14, 83))
|
|
100
|
+
self.assertTrue(all((r * r - 2) % 97 == 0 for r in roots_97))
|
|
101
|
+
|
|
102
|
+
def test_rejects_composite_odd_p(self):
|
|
103
|
+
for comp in (9, 15, 25, 561):
|
|
104
|
+
with self.subTest(p=comp):
|
|
105
|
+
with self.assertRaises(ValueError):
|
|
106
|
+
sqrt_mod(4, comp)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestPrimality(unittest.TestCase):
|
|
110
|
+
def test_small_primes(self):
|
|
111
|
+
for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 97, 193):
|
|
112
|
+
with self.subTest(p=p):
|
|
113
|
+
self.assertTrue(_is_probable_prime(p))
|
|
114
|
+
|
|
115
|
+
def test_small_composites(self):
|
|
116
|
+
for c in (0, 1, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 25, 27):
|
|
117
|
+
with self.subTest(c=c):
|
|
118
|
+
self.assertFalse(_is_probable_prime(c))
|
|
119
|
+
|
|
120
|
+
def test_carmichael_numbers_rejected(self):
|
|
121
|
+
for cn in (561, 1105, 1729):
|
|
122
|
+
with self.subTest(cn=cn):
|
|
123
|
+
self.assertFalse(_is_probable_prime(cn))
|
|
124
|
+
|
|
125
|
+
def test_mersenne_prime(self):
|
|
126
|
+
self.assertTrue(_is_probable_prime(2147483647))
|
|
127
|
+
|
|
54
128
|
|
|
55
129
|
class TestEulerHermannPiApprox(unittest.TestCase):
|
|
56
130
|
def test_converges_to_pi(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|