foxmath 0.2.0__tar.gz → 0.4.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.2.0
3
+ Version: 0.4.0
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -28,11 +28,13 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
28
28
  ## Features
29
29
  - Legendre symbol
30
30
  - Euler/Hermann Machin-like π approximation
31
- - Elliptic curve point addition (over finite fields)
31
+ - Elliptic curve point addition (over finite fields), including
32
+ named curves (secp256k1) with on-curve result validation
32
33
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
33
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt)
34
+ - Continued fraction expansion and convergents
35
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
34
36
  - JSON output mode
35
- - Coming soon: continued fractions, challenge mode, more curves
37
+ - Coming soon: challenge mode
36
38
 
37
39
  ## Installation
38
40
 
@@ -71,6 +73,15 @@ foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
71
73
  Result point: (14, 2)
72
74
  ```
73
75
 
76
+ ```bash
77
+ # Named curve (secp256k1) — computes 2G by default
78
+ foxmath ecadd --curve secp256k1
79
+ ```
80
+ ```
81
+ Result point: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)
82
+ ✓ on curve
83
+ ```
84
+
74
85
  ```bash
75
86
  # Chinese Remainder Theorem
76
87
  foxmath crt --r 2 3 2 --m 3 5 7
@@ -79,6 +90,17 @@ foxmath crt --r 2 3 2 --m 3 5 7
79
90
  x ≡ 23 (mod 105)
80
91
  ```
81
92
 
93
+ ```bash
94
+ # Continued fraction expansion + convergents
95
+ foxmath cf --num 355 --den 113
96
+ ```
97
+ ```
98
+ [3; 7, 16]
99
+ 3/1
100
+ 22/7
101
+ 355/113
102
+ ```
103
+
82
104
  ```bash
83
105
  # JSON output
84
106
  foxmath pi --terms 100 --json
@@ -9,11 +9,13 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
9
9
  ## Features
10
10
  - Legendre symbol
11
11
  - Euler/Hermann Machin-like π approximation
12
- - Elliptic curve point addition (over finite fields)
12
+ - Elliptic curve point addition (over finite fields), including
13
+ named curves (secp256k1) with on-curve result validation
13
14
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
14
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt)
15
+ - Continued fraction expansion and convergents
16
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
15
17
  - JSON output mode
16
- - Coming soon: continued fractions, challenge mode, more curves
18
+ - Coming soon: challenge mode
17
19
 
18
20
  ## Installation
19
21
 
@@ -52,6 +54,15 @@ foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
52
54
  Result point: (14, 2)
53
55
  ```
54
56
 
57
+ ```bash
58
+ # Named curve (secp256k1) — computes 2G by default
59
+ foxmath ecadd --curve secp256k1
60
+ ```
61
+ ```
62
+ Result point: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)
63
+ ✓ on curve
64
+ ```
65
+
55
66
  ```bash
56
67
  # Chinese Remainder Theorem
57
68
  foxmath crt --r 2 3 2 --m 3 5 7
@@ -60,6 +71,17 @@ foxmath crt --r 2 3 2 --m 3 5 7
60
71
  x ≡ 23 (mod 105)
61
72
  ```
62
73
 
74
+ ```bash
75
+ # Continued fraction expansion + convergents
76
+ foxmath cf --num 355 --den 113
77
+ ```
78
+ ```
79
+ [3; 7, 16]
80
+ 3/1
81
+ 22/7
82
+ 355/113
83
+ ```
84
+
63
85
  ```bash
64
86
  # JSON output
65
87
  foxmath pi --terms 100 --json
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxmath"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Minimalist CLI for math & cryptography exploration 🦊"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -32,3 +32,4 @@ foxmath-legendre = "foxmath.foxmath:main"
32
32
  foxmath-pi = "foxmath.foxmath:main"
33
33
  foxmath-ecadd = "foxmath.foxmath:main"
34
34
  foxmath-crt = "foxmath.foxmath:main"
35
+ foxmath-cf = "foxmath.foxmath:main"
@@ -54,6 +54,28 @@ def ec_point_add(x1, y1, x2, y2, a, p):
54
54
  y3 = (lam * (x1 - x3) - y1) % p
55
55
  return x3, y3
56
56
 
57
+ # Named curve presets: a, b, p, and the standard generator point G (if defined).
58
+ CURVES = {
59
+ "secp256k1": {
60
+ "a": 0,
61
+ "b": 7,
62
+ "p": 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
63
+ "gx": 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
64
+ "gy": 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
65
+ },
66
+ "toy97": { # small curve for quick hand-checkable examples: y^2 = x^3 + 2x + 3 (mod 97)
67
+ "a": 2,
68
+ "b": 3,
69
+ "p": 97,
70
+ "gx": 3,
71
+ "gy": 6,
72
+ },
73
+ }
74
+
75
+ def is_on_curve(x: int, y: int, a: int, b: int, p: int) -> bool:
76
+ """Check y^2 = x^3 + a*x + b (mod p)."""
77
+ return (y * y - (x ** 3 + a * x + b)) % p == 0
78
+
57
79
  def _extended_gcd(a: int, b: int):
58
80
  """Returns (g, x, y) such that a*x + b*y == g == gcd(a, b)."""
59
81
  old_r, r = a, b
@@ -96,6 +118,38 @@ def crt_solve(remainders, moduli):
96
118
  r, m = _combine_crt(r, m, ri % mi, mi)
97
119
  return r, m
98
120
 
121
+ def continued_fraction(num: int, den: int, max_terms: int = 100):
122
+ """
123
+ Continued fraction expansion [a0; a1, a2, ...] of num/den via the
124
+ Euclidean algorithm. Terminates naturally for any rational input.
125
+ """
126
+ if den == 0:
127
+ raise ValueError("denominator cannot be zero")
128
+ cf = []
129
+ n, d = num, den
130
+ while d != 0 and len(cf) < max_terms:
131
+ a = n // d
132
+ cf.append(a)
133
+ n, d = d, n - a * d
134
+ return cf
135
+
136
+ def cf_convergents(cf):
137
+ """
138
+ Convergents (p_n, q_n) of a continued fraction, via the standard
139
+ recurrence h_n = a_n*h_(n-1) + h_(n-2), same for k_n, with
140
+ h_(-2)=0, h_(-1)=1, k_(-2)=1, k_(-1)=0.
141
+ """
142
+ convergents = []
143
+ h2, h1 = 0, 1
144
+ k2, k1 = 1, 0
145
+ for a in cf:
146
+ h = a * h1 + h2
147
+ k = a * k1 + k2
148
+ convergents.append((h, k))
149
+ h2, h1 = h1, h
150
+ k2, k1 = k1, k
151
+ return convergents
152
+
99
153
  # ------------------- CLI -------------------
100
154
 
101
155
  def get_command():
@@ -139,18 +193,26 @@ def main():
139
193
 
140
194
  # Elliptic curve add
141
195
  ec = subparsers.add_parser("ecadd", help="Elliptic curve point addition")
142
- ec.add_argument("--x1", type=int, required=True)
143
- ec.add_argument("--y1", type=int, required=True)
144
- ec.add_argument("--x2", type=int, required=True)
145
- ec.add_argument("--y2", type=int, required=True)
146
- ec.add_argument("--a", type=int, default=-3, help="Curve parameter a")
147
- ec.add_argument("--p", type=int, required=True, help="Prime modulus")
196
+ ec.add_argument("--curve", choices=sorted(CURVES.keys()),
197
+ help="Named curve (provides a, b, p, and generator G)")
198
+ ec.add_argument("--x1", type=int, help="Defaults to G's x if --curve is given")
199
+ ec.add_argument("--y1", type=int, help="Defaults to G's y if --curve is given")
200
+ ec.add_argument("--x2", type=int, help="Defaults to (x1,y1) if omitted (i.e. doubling)")
201
+ ec.add_argument("--y2", type=int, help="Defaults to (x1,y1) if omitted (i.e. doubling)")
202
+ ec.add_argument("--a", type=int, help="Curve parameter a (overrides --curve if given)")
203
+ ec.add_argument("--p", type=int, help="Prime modulus (overrides --curve if given)")
148
204
 
149
205
  # Chinese Remainder Theorem
150
206
  crt = subparsers.add_parser("crt", help="Solve a system of congruences x = r (mod m)")
151
207
  crt.add_argument("--r", type=int, nargs="+", required=True, help="Remainders, e.g. --r 2 3 2")
152
208
  crt.add_argument("--m", type=int, nargs="+", required=True, help="Moduli, e.g. --m 3 5 7")
153
209
 
210
+ # Continued fractions
211
+ cf_cmd = subparsers.add_parser("cf", help="Continued fraction expansion and convergents")
212
+ cf_cmd.add_argument("--num", type=int, required=True, help="Numerator")
213
+ cf_cmd.add_argument("--den", type=int, required=True, help="Denominator")
214
+ cf_cmd.add_argument("--terms", type=int, default=100, help="Max terms to expand")
215
+
154
216
  args = parser.parse_args(argv)
155
217
 
156
218
  result = {"tool": "foxmath"}
@@ -169,10 +231,41 @@ def main():
169
231
  print(f"π ≈ {approx} ({args.terms} terms)")
170
232
 
171
233
  elif cmd == "ecadd" or args.command == "ecadd":
172
- x3, y3 = ec_point_add(args.x1, args.y1, args.x2, args.y2, args.a, args.p)
173
- result.update({"command": "ecadd", "result": (x3, y3)})
234
+ a, b = args.a, None
235
+ p = args.p
236
+ x1, y1 = args.x1, args.y1
237
+
238
+ if args.curve:
239
+ preset = CURVES[args.curve]
240
+ a = preset["a"] if a is None else a
241
+ p = preset["p"] if p is None else p
242
+ b = preset["b"]
243
+ if x1 is None:
244
+ x1, y1 = preset["gx"], preset["gy"]
245
+ elif a is None:
246
+ a = -3 # historical default for manual (non-curve) mode
247
+
248
+ if a is None or p is None or x1 is None or y1 is None:
249
+ raise ValueError(
250
+ "Provide --curve NAME, or at least --p --x1 --y1 manually "
251
+ "(--a defaults to -3 if omitted)."
252
+ )
253
+
254
+ x2 = x1 if args.x2 is None else args.x2
255
+ y2 = y1 if args.y2 is None else args.y2
256
+
257
+ x3, y3 = ec_point_add(x1, y1, x2, y2, a, p)
258
+ result.update({
259
+ "command": "ecadd", "curve": args.curve,
260
+ "p1": [x1, y1], "p2": [x2, y2], "result": (x3, y3),
261
+ })
262
+ if b is not None:
263
+ result["result_on_curve"] = is_on_curve(x3, y3, a, b, p)
174
264
  if not json_output:
175
265
  print(f"Result point: ({x3}, {y3})")
266
+ if b is not None:
267
+ status = "✓ on curve" if result["result_on_curve"] else "✗ NOT on curve"
268
+ print(f" {status}")
176
269
 
177
270
  elif cmd == "crt" or args.command == "crt":
178
271
  x, m = crt_solve(args.r, args.m)
@@ -180,6 +273,18 @@ def main():
180
273
  if not json_output:
181
274
  print(f"x ≡ {x} (mod {m})")
182
275
 
276
+ elif cmd == "cf" or args.command == "cf":
277
+ cf = continued_fraction(args.num, args.den, args.terms)
278
+ convs = cf_convergents(cf)
279
+ result.update({
280
+ "command": "cf", "num": args.num, "den": args.den,
281
+ "cf": cf, "convergents": convs,
282
+ })
283
+ if not json_output:
284
+ print(f"[{cf[0]}; {', '.join(str(a) for a in cf[1:])}]")
285
+ for p, q in convs:
286
+ print(f" {p}/{q}")
287
+
183
288
  except Exception as e:
184
289
  print(f"Error: {e}", file=sys.stderr)
185
290
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -28,11 +28,13 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
28
28
  ## Features
29
29
  - Legendre symbol
30
30
  - Euler/Hermann Machin-like π approximation
31
- - Elliptic curve point addition (over finite fields)
31
+ - Elliptic curve point addition (over finite fields), including
32
+ named curves (secp256k1) with on-curve result validation
32
33
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
33
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt)
34
+ - Continued fraction expansion and convergents
35
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
34
36
  - JSON output mode
35
- - Coming soon: continued fractions, challenge mode, more curves
37
+ - Coming soon: challenge mode
36
38
 
37
39
  ## Installation
38
40
 
@@ -71,6 +73,15 @@ foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
71
73
  Result point: (14, 2)
72
74
  ```
73
75
 
76
+ ```bash
77
+ # Named curve (secp256k1) — computes 2G by default
78
+ foxmath ecadd --curve secp256k1
79
+ ```
80
+ ```
81
+ Result point: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)
82
+ ✓ on curve
83
+ ```
84
+
74
85
  ```bash
75
86
  # Chinese Remainder Theorem
76
87
  foxmath crt --r 2 3 2 --m 3 5 7
@@ -79,6 +90,17 @@ foxmath crt --r 2 3 2 --m 3 5 7
79
90
  x ≡ 23 (mod 105)
80
91
  ```
81
92
 
93
+ ```bash
94
+ # Continued fraction expansion + convergents
95
+ foxmath cf --num 355 --den 113
96
+ ```
97
+ ```
98
+ [3; 7, 16]
99
+ 3/1
100
+ 22/7
101
+ 355/113
102
+ ```
103
+
82
104
  ```bash
83
105
  # JSON output
84
106
  foxmath pi --terms 100 --json
@@ -1,5 +1,6 @@
1
1
  [console_scripts]
2
2
  foxmath = foxmath.foxmath:main
3
+ foxmath-cf = foxmath.foxmath:main
3
4
  foxmath-crt = foxmath.foxmath:main
4
5
  foxmath-ecadd = foxmath.foxmath:main
5
6
  foxmath-legendre = foxmath.foxmath:main
@@ -85,6 +85,12 @@ class TestSymlinkInvocation(unittest.TestCase):
85
85
  self.assertEqual(code, 0)
86
86
  self.assertIn("x ≡ 23 (mod 105)", out)
87
87
 
88
+ def test_cf_symlink(self):
89
+ script = self._make_symlink_script("foxmath-cf")
90
+ code, out, err = run("--num", "355", "--den", "113", script=script)
91
+ self.assertEqual(code, 0)
92
+ self.assertIn("[3; 7, 16]", out)
93
+
88
94
  def test_symlink_with_json(self):
89
95
  script = self._make_symlink_script("foxmath-legendre")
90
96
  code, out, err = run("2", "7", "--json", script=script)
@@ -104,6 +110,29 @@ class TestNormalUsage(unittest.TestCase):
104
110
  self.assertEqual(code, 1)
105
111
  self.assertIn("point at infinity", err)
106
112
 
113
+ def test_ecadd_curve_secp256k1_defaults_to_doubling_generator(self):
114
+ code, out, err = run("ecadd", "--curve", "secp256k1")
115
+ self.assertEqual(code, 0)
116
+ self.assertIn("89565891926547004231252920425935692360644145829622209833684329913297188986597", out)
117
+ self.assertIn("on curve", out)
118
+
119
+ def test_ecadd_curve_manual_a_override(self):
120
+ # --a explicitly provided should override the curve preset's a.
121
+ code, out, err = run("ecadd", "--curve", "toy97", "--a", "2")
122
+ self.assertEqual(code, 0)
123
+
124
+ def test_ecadd_no_args_at_all_errors_clearly(self):
125
+ code, out, err = run("ecadd")
126
+ self.assertEqual(code, 1)
127
+ self.assertIn("Error", err)
128
+
129
+ def test_ecadd_backward_compatible_manual_call(self):
130
+ # Original documented usage: --a omitted, defaults to -3.
131
+ code, out, err = run("ecadd", "--x1", "1", "--y1", "2",
132
+ "--x2", "3", "--y2", "4", "--p", "17")
133
+ self.assertEqual(code, 0)
134
+ self.assertIn("Result point: (14, 2)", out)
135
+
107
136
  def test_crt(self):
108
137
  code, out, err = run("crt", "--r", "2", "3", "2", "--m", "3", "5", "7")
109
138
  self.assertEqual(code, 0)
@@ -122,6 +151,25 @@ class TestNormalUsage(unittest.TestCase):
122
151
  self.assertEqual(code, 1)
123
152
  self.assertIn("inconsistent", err)
124
153
 
154
+ def test_cf(self):
155
+ code, out, err = run("cf", "--num", "355", "--den", "113")
156
+ self.assertEqual(code, 0)
157
+ self.assertIn("[3; 7, 16]", out)
158
+ self.assertIn("355/113", out)
159
+
160
+ def test_cf_json_is_pure_json(self):
161
+ code, out, err = run("cf", "--num", "355", "--den", "113", "--json")
162
+ self.assertEqual(code, 0)
163
+ import json
164
+ parsed = json.loads(out)
165
+ self.assertEqual(parsed["cf"], [3, 7, 16])
166
+ self.assertEqual(parsed["convergents"][-1], [355, 113])
167
+
168
+ def test_cf_zero_denominator_errors(self):
169
+ code, out, err = run("cf", "--num", "5", "--den", "0")
170
+ self.assertEqual(code, 1)
171
+ self.assertIn("Error", err)
172
+
125
173
 
126
174
  if __name__ == "__main__":
127
175
  unittest.main()
@@ -12,7 +12,10 @@ from decimal import Decimal
12
12
 
13
13
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
14
14
 
15
- from foxmath.foxmath import legendre_symbol, euler_hermann_pi_approx, ec_point_add, crt_solve
15
+ from foxmath.foxmath import (
16
+ legendre_symbol, euler_hermann_pi_approx, ec_point_add, crt_solve,
17
+ continued_fraction, cf_convergents, CURVES, is_on_curve,
18
+ )
16
19
 
17
20
  # Reference pi digits (mpmath, 250 dps of working precision), "3" + fractional digits.
18
21
  PI_REFERENCE = Decimal(
@@ -115,5 +118,62 @@ class TestCRT(unittest.TestCase):
115
118
  self.assertEqual(x % 7, 3)
116
119
 
117
120
 
121
+ class TestContinuedFractions(unittest.TestCase):
122
+ def test_classic_pi_approximation(self):
123
+ cf = continued_fraction(355, 113)
124
+ self.assertEqual(cf, [3, 7, 16])
125
+ convs = cf_convergents(cf)
126
+ self.assertEqual(convs, [(3, 1), (22, 7), (355, 113)])
127
+
128
+ def test_convergents_reduce_to_final_fraction(self):
129
+ cf = continued_fraction(19, 7)
130
+ convs = cf_convergents(cf)
131
+ self.assertEqual(convs[-1], (19, 7))
132
+
133
+ def test_negative_numerator(self):
134
+ cf = continued_fraction(-19, 7)
135
+ convs = cf_convergents(cf)
136
+ self.assertEqual(convs[-1], (-19, 7))
137
+
138
+ def test_zero_denominator_raises(self):
139
+ with self.assertRaises(ValueError):
140
+ continued_fraction(5, 0)
141
+
142
+ def test_randomized_against_fractions_module(self):
143
+ import random
144
+ from fractions import Fraction
145
+ random.seed(42)
146
+ for _ in range(200):
147
+ n = random.randint(-10000, 10000)
148
+ d = random.randint(1, 10000)
149
+ cf = continued_fraction(n, d)
150
+ convs = cf_convergents(cf)
151
+ expected = Fraction(n, d)
152
+ self.assertEqual(convs[-1], (expected.numerator, expected.denominator))
153
+
154
+
155
+ class TestNamedCurves(unittest.TestCase):
156
+ def test_secp256k1_generator_is_on_curve(self):
157
+ c = CURVES["secp256k1"]
158
+ self.assertTrue(is_on_curve(c["gx"], c["gy"], c["a"], c["b"], c["p"]))
159
+
160
+ def test_secp256k1_doubling_matches_known_value(self):
161
+ # Cross-checked independently against the `ecdsa` library's
162
+ # SECP256k1.generator * 2.
163
+ c = CURVES["secp256k1"]
164
+ x2, y2 = ec_point_add(c["gx"], c["gy"], c["gx"], c["gy"], c["a"], c["p"])
165
+ self.assertEqual(
166
+ x2, 89565891926547004231252920425935692360644145829622209833684329913297188986597
167
+ )
168
+ self.assertEqual(
169
+ y2, 12158399299693830322967808612713398636155367887041628176798871954788371653930
170
+ )
171
+ self.assertTrue(is_on_curve(x2, y2, c["a"], c["b"], c["p"]))
172
+
173
+ def test_toy97_generator_is_on_curve(self):
174
+ c = CURVES["toy97"]
175
+ self.assertTrue(is_on_curve(c["gx"], c["gy"], c["a"], c["b"], c["p"]))
176
+
177
+
118
178
  if __name__ == "__main__":
119
179
  unittest.main()
File without changes
File without changes
File without changes