foxmath 0.6.2__tar.gz → 0.7.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.6.2
3
+ Version: 0.7.0
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-pi, foxmath-ecadd,
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.6.2
64
+ foxmath 0.7.0
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-pi, foxmath-ecadd,
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.6.2
45
+ foxmath 0.7.0
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.6.2"
7
+ version = "0.7.0"
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"
@@ -29,6 +29,54 @@ def legendre_symbol(a: int, p: int) -> int:
29
29
  res = pow(a, (p - 1) // 2, p)
30
30
  return 1 if res == 1 else (-1 if res == p - 1 else 0)
31
31
 
32
+ def sqrt_mod(a: int, p: int) -> tuple:
33
+ """Return the square root(s) of a modulo an odd prime p."""
34
+ if p < 2 or p % 2 == 0:
35
+ raise ValueError("p must be an odd prime")
36
+
37
+ a %= p
38
+ symbol = legendre_symbol(a, p)
39
+ if symbol == 0:
40
+ return (0,)
41
+ if symbol == -1:
42
+ raise ValueError(f"{a} has no square root modulo {p}")
43
+
44
+ # For p ≡ 3 (mod 4), the root is a^(p+1)/4.
45
+ if p % 4 == 3:
46
+ root = pow(a, (p + 1) // 4, p)
47
+ else:
48
+ # Tonelli-Shanks: write p - 1 = q * 2^s with q odd.
49
+ q, s = p - 1, 0
50
+ while q % 2 == 0:
51
+ q //= 2
52
+ s += 1
53
+
54
+ non_residue = 2
55
+ while legendre_symbol(non_residue, p) != -1:
56
+ non_residue += 1
57
+
58
+ c = pow(non_residue, q, p)
59
+ root = pow(a, (q + 1) // 2, p)
60
+ t = pow(a, q, p)
61
+ m = s
62
+
63
+ while t != 1:
64
+ i = 1
65
+ t_squared = (t * t) % p
66
+ while t_squared != 1:
67
+ t_squared = (t_squared * t_squared) % p
68
+ i += 1
69
+ if i == m:
70
+ raise ValueError(f"{a} has no square root modulo {p}")
71
+ b = pow(c, 1 << (m - i - 1), p)
72
+ root = (root * b) % p
73
+ t = (t * b * b) % p
74
+ c = (b * b) % p
75
+ m = i
76
+
77
+ other = (-root) % p
78
+ return tuple(sorted({root, other}))
79
+
32
80
  def _arctan_series(inv_n: Decimal, terms: int) -> Decimal:
33
81
  """arctan(1/n) via its Taylor series, evaluated to `terms` terms. `inv_n` is 1/n."""
34
82
  total = Decimal(0)
@@ -283,6 +331,17 @@ def get_command():
283
331
  return None
284
332
 
285
333
  def main():
334
+ if hasattr(sys.stdout, "reconfigure"):
335
+ try:
336
+ sys.stdout.reconfigure(encoding="utf-8")
337
+ except Exception:
338
+ pass
339
+ if hasattr(sys.stderr, "reconfigure"):
340
+ try:
341
+ sys.stderr.reconfigure(encoding="utf-8")
342
+ except Exception:
343
+ pass
344
+
286
345
  cmd = get_command()
287
346
 
288
347
  # Handle --json independently of argparse subparser scoping: a subparser's
@@ -311,6 +370,10 @@ def main():
311
370
  leg.add_argument("a", type=int, help="Integer a")
312
371
  leg.add_argument("p", type=int, help="Odd prime p")
313
372
 
373
+ sqrt = subparsers.add_parser("sqrtmod", help="Square root(s) modulo an odd prime")
374
+ sqrt.add_argument("a", type=int, help="Integer a")
375
+ sqrt.add_argument("p", type=int, help="Odd prime p")
376
+
314
377
  # Pi
315
378
  pi_cmd = subparsers.add_parser("pi", help="Euler/Hermann Machin-like π approximation")
316
379
  pi_cmd.add_argument("--terms", type=int, default=100)
@@ -366,6 +429,15 @@ def main():
366
429
  if not json_output:
367
430
  print(f"Legendre ({args.a}/{args.p}) = {ls}")
368
431
 
432
+ elif cmd == "sqrtmod" or args.command == "sqrtmod":
433
+ roots = sqrt_mod(args.a, args.p)
434
+ result.update({"command": "sqrtmod", "a": args.a, "p": args.p, "roots": list(roots)})
435
+ if not json_output:
436
+ if len(roots) == 1:
437
+ print(f"sqrt({args.a}) = {roots[0]} (mod {args.p})")
438
+ else:
439
+ print(f"sqrt({args.a}) = +/-{roots[1]} (mod {args.p})")
440
+
369
441
  elif cmd == "pi" or args.command == "pi":
370
442
  approx = euler_hermann_pi_approx(args.terms)
371
443
  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.6.2
3
+ Version: 0.7.0
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-pi, foxmath-ecadd,
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.6.2
64
+ foxmath 0.7.0
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
@@ -7,3 +7,4 @@ foxmath-ecadd = foxmath.foxmath:main
7
7
  foxmath-ecmul = foxmath.foxmath:main
8
8
  foxmath-legendre = foxmath.foxmath:main
9
9
  foxmath-pi = foxmath.foxmath:main
10
+ foxmath-sqrtmod = foxmath.foxmath:main
@@ -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,16 @@ 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
+
278
304
 
279
305
  if __name__ == "__main__":
280
306
  unittest.main()
@@ -13,7 +13,7 @@ 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
18
  generate_problem,
19
19
  )
@@ -52,6 +52,48 @@ class TestLegendreSymbol(unittest.TestCase):
52
52
  legendre_symbol(3, 1)
53
53
 
54
54
 
55
+ class TestSqrtMod(unittest.TestCase):
56
+ def test_tonelli_shanks_returns_both_roots(self):
57
+ roots = sqrt_mod(10, 13)
58
+ self.assertEqual(roots, (6, 7))
59
+ self.assertTrue(all((root * root - 10) % 13 == 0 for root in roots))
60
+
61
+ def test_fast_path_for_prime_three_mod_four(self):
62
+ self.assertEqual(sqrt_mod(9, 11), (3, 8))
63
+
64
+ def test_zero_has_one_root(self):
65
+ self.assertEqual(sqrt_mod(0, 17), (0,))
66
+
67
+ def test_rejects_non_residue(self):
68
+ with self.assertRaises(ValueError):
69
+ sqrt_mod(5, 17)
70
+
71
+ def test_rejects_even_p(self):
72
+ with self.assertRaises(ValueError):
73
+ sqrt_mod(1, 2)
74
+
75
+ def test_rejects_p_less_than_2(self):
76
+ with self.assertRaises(ValueError):
77
+ sqrt_mod(1, 1)
78
+ with self.assertRaises(ValueError):
79
+ sqrt_mod(1, -5)
80
+
81
+ def test_normalizes_a_greater_than_p(self):
82
+ self.assertEqual(sqrt_mod(10 + 26, 13), (6, 7))
83
+
84
+ def test_negative_a(self):
85
+ self.assertEqual(sqrt_mod(-3, 13), (6, 7))
86
+
87
+ def test_larger_primes_tonelli_shanks(self):
88
+ roots_41 = sqrt_mod(8, 41)
89
+ self.assertEqual(roots_41, (7, 34))
90
+ self.assertTrue(all((r * r - 8) % 41 == 0 for r in roots_41))
91
+
92
+ roots_97 = sqrt_mod(2, 97)
93
+ self.assertEqual(roots_97, (14, 83))
94
+ self.assertTrue(all((r * r - 2) % 97 == 0 for r in roots_97))
95
+
96
+
55
97
  class TestEulerHermannPiApprox(unittest.TestCase):
56
98
  def test_converges_to_pi(self):
57
99
  # Regression check: this used to converge to 16*arctan(1/2) =~ 7.418,
File without changes
File without changes
File without changes