foxmath 0.3.0__tar.gz → 0.5.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.
- {foxmath-0.3.0/src/foxmath.egg-info → foxmath-0.5.0}/PKG-INFO +30 -4
- {foxmath-0.3.0 → foxmath-0.5.0}/README.md +29 -3
- {foxmath-0.3.0 → foxmath-0.5.0}/pyproject.toml +2 -1
- {foxmath-0.3.0 → foxmath-0.5.0}/src/foxmath/foxmath.py +155 -8
- {foxmath-0.3.0 → foxmath-0.5.0/src/foxmath.egg-info}/PKG-INFO +30 -4
- {foxmath-0.3.0 → foxmath-0.5.0}/src/foxmath.egg-info/entry_points.txt +1 -0
- {foxmath-0.3.0 → foxmath-0.5.0}/tests/test_cli.py +72 -2
- {foxmath-0.3.0 → foxmath-0.5.0}/tests/test_foxmath.py +57 -1
- {foxmath-0.3.0 → foxmath-0.5.0}/LICENSE +0 -0
- {foxmath-0.3.0 → foxmath-0.5.0}/setup.cfg +0 -0
- {foxmath-0.3.0 → foxmath-0.5.0}/src/foxmath/__init__.py +0 -0
- {foxmath-0.3.0 → foxmath-0.5.0}/src/foxmath.egg-info/SOURCES.txt +0 -0
- {foxmath-0.3.0 → foxmath-0.5.0}/src/foxmath.egg-info/dependency_links.txt +0 -0
- {foxmath-0.3.0 → foxmath-0.5.0}/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.5.0
|
|
4
4
|
Summary: Minimalist CLI for math & cryptography exploration 🦊
|
|
5
5
|
Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,12 +28,15 @@ 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
34
|
- Continued fraction expansion and convergents
|
|
34
|
-
-
|
|
35
|
+
- Challenge/quiz mode — practice problems with scoring, or a
|
|
36
|
+
worksheet mode for self-study
|
|
37
|
+
- Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd,
|
|
38
|
+
foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
35
39
|
- JSON output mode
|
|
36
|
-
- Coming soon: challenge mode, more curves
|
|
37
40
|
|
|
38
41
|
## Installation
|
|
39
42
|
|
|
@@ -72,6 +75,15 @@ foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
|
|
|
72
75
|
Result point: (14, 2)
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
```bash
|
|
79
|
+
# Named curve (secp256k1) — computes 2G by default
|
|
80
|
+
foxmath ecadd --curve secp256k1
|
|
81
|
+
```
|
|
82
|
+
```
|
|
83
|
+
Result point: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)
|
|
84
|
+
✓ on curve
|
|
85
|
+
```
|
|
86
|
+
|
|
75
87
|
```bash
|
|
76
88
|
# Chinese Remainder Theorem
|
|
77
89
|
foxmath crt --r 2 3 2 --m 3 5 7
|
|
@@ -91,6 +103,20 @@ foxmath cf --num 355 --den 113
|
|
|
91
103
|
355/113
|
|
92
104
|
```
|
|
93
105
|
|
|
106
|
+
```bash
|
|
107
|
+
# Challenge/quiz mode — worksheet (no prompting)
|
|
108
|
+
foxmath challenge --topic legendre --count 2 --seed 1 --reveal
|
|
109
|
+
```
|
|
110
|
+
```
|
|
111
|
+
1. Legendre symbol (10/13)
|
|
112
|
+
Answer: 1
|
|
113
|
+
2. Legendre symbol (3/7)
|
|
114
|
+
Answer: -1
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Drop `--reveal` to answer interactively instead, with scoring at the end.
|
|
118
|
+
`--topic` accepts `legendre`, `crt`, or `mixed` (default).
|
|
119
|
+
|
|
94
120
|
```bash
|
|
95
121
|
# JSON output
|
|
96
122
|
foxmath pi --terms 100 --json
|
|
@@ -9,12 +9,15 @@ 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
15
|
- Continued fraction expansion and convergents
|
|
15
|
-
-
|
|
16
|
+
- Challenge/quiz mode — practice problems with scoring, or a
|
|
17
|
+
worksheet mode for self-study
|
|
18
|
+
- Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd,
|
|
19
|
+
foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
16
20
|
- JSON output mode
|
|
17
|
-
- Coming soon: challenge mode, more curves
|
|
18
21
|
|
|
19
22
|
## Installation
|
|
20
23
|
|
|
@@ -53,6 +56,15 @@ foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
|
|
|
53
56
|
Result point: (14, 2)
|
|
54
57
|
```
|
|
55
58
|
|
|
59
|
+
```bash
|
|
60
|
+
# Named curve (secp256k1) — computes 2G by default
|
|
61
|
+
foxmath ecadd --curve secp256k1
|
|
62
|
+
```
|
|
63
|
+
```
|
|
64
|
+
Result point: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)
|
|
65
|
+
✓ on curve
|
|
66
|
+
```
|
|
67
|
+
|
|
56
68
|
```bash
|
|
57
69
|
# Chinese Remainder Theorem
|
|
58
70
|
foxmath crt --r 2 3 2 --m 3 5 7
|
|
@@ -72,6 +84,20 @@ foxmath cf --num 355 --den 113
|
|
|
72
84
|
355/113
|
|
73
85
|
```
|
|
74
86
|
|
|
87
|
+
```bash
|
|
88
|
+
# Challenge/quiz mode — worksheet (no prompting)
|
|
89
|
+
foxmath challenge --topic legendre --count 2 --seed 1 --reveal
|
|
90
|
+
```
|
|
91
|
+
```
|
|
92
|
+
1. Legendre symbol (10/13)
|
|
93
|
+
Answer: 1
|
|
94
|
+
2. Legendre symbol (3/7)
|
|
95
|
+
Answer: -1
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Drop `--reveal` to answer interactively instead, with scoring at the end.
|
|
99
|
+
`--topic` accepts `legendre`, `crt`, or `mixed` (default).
|
|
100
|
+
|
|
75
101
|
```bash
|
|
76
102
|
# JSON output
|
|
77
103
|
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.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Minimalist CLI for math & cryptography exploration 🦊"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -33,3 +33,4 @@ foxmath-pi = "foxmath.foxmath:main"
|
|
|
33
33
|
foxmath-ecadd = "foxmath.foxmath:main"
|
|
34
34
|
foxmath-crt = "foxmath.foxmath:main"
|
|
35
35
|
foxmath-cf = "foxmath.foxmath:main"
|
|
36
|
+
foxmath-challenge = "foxmath.foxmath:main"
|
|
@@ -7,7 +7,9 @@ One binary. Clean. Educational. Safe.
|
|
|
7
7
|
import argparse
|
|
8
8
|
import sys
|
|
9
9
|
import json
|
|
10
|
+
import random
|
|
10
11
|
from decimal import Decimal, getcontext
|
|
12
|
+
from math import gcd
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
|
|
13
15
|
# ------------------- Math Functions -------------------
|
|
@@ -54,6 +56,28 @@ def ec_point_add(x1, y1, x2, y2, a, p):
|
|
|
54
56
|
y3 = (lam * (x1 - x3) - y1) % p
|
|
55
57
|
return x3, y3
|
|
56
58
|
|
|
59
|
+
# Named curve presets: a, b, p, and the standard generator point G (if defined).
|
|
60
|
+
CURVES = {
|
|
61
|
+
"secp256k1": {
|
|
62
|
+
"a": 0,
|
|
63
|
+
"b": 7,
|
|
64
|
+
"p": 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
|
|
65
|
+
"gx": 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
|
|
66
|
+
"gy": 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
|
|
67
|
+
},
|
|
68
|
+
"toy97": { # small curve for quick hand-checkable examples: y^2 = x^3 + 2x + 3 (mod 97)
|
|
69
|
+
"a": 2,
|
|
70
|
+
"b": 3,
|
|
71
|
+
"p": 97,
|
|
72
|
+
"gx": 3,
|
|
73
|
+
"gy": 6,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def is_on_curve(x: int, y: int, a: int, b: int, p: int) -> bool:
|
|
78
|
+
"""Check y^2 = x^3 + a*x + b (mod p)."""
|
|
79
|
+
return (y * y - (x ** 3 + a * x + b)) % p == 0
|
|
80
|
+
|
|
57
81
|
def _extended_gcd(a: int, b: int):
|
|
58
82
|
"""Returns (g, x, y) such that a*x + b*y == g == gcd(a, b)."""
|
|
59
83
|
old_r, r = a, b
|
|
@@ -128,6 +152,46 @@ def cf_convergents(cf):
|
|
|
128
152
|
k2, k1 = k1, k
|
|
129
153
|
return convergents
|
|
130
154
|
|
|
155
|
+
# ------------------- Challenge / Quiz Mode -------------------
|
|
156
|
+
|
|
157
|
+
_SMALL_ODD_PRIMES = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
|
|
158
|
+
53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
|
|
159
|
+
_CRT_MODULI_CANDIDATES = [3, 4, 5, 7, 9, 11, 13]
|
|
160
|
+
|
|
161
|
+
def _generate_legendre_problem(rng: random.Random) -> dict:
|
|
162
|
+
p = rng.choice(_SMALL_ODD_PRIMES)
|
|
163
|
+
a = rng.randint(1, p - 1)
|
|
164
|
+
return {
|
|
165
|
+
"topic": "legendre",
|
|
166
|
+
"question": f"Legendre symbol ({a}/{p})",
|
|
167
|
+
"answer": legendre_symbol(a, p),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def _generate_crt_problem(rng: random.Random) -> dict:
|
|
171
|
+
while True:
|
|
172
|
+
m1, m2 = rng.sample(_CRT_MODULI_CANDIDATES, 2)
|
|
173
|
+
if gcd(m1, m2) == 1:
|
|
174
|
+
break
|
|
175
|
+
r1, r2 = rng.randrange(m1), rng.randrange(m2)
|
|
176
|
+
x, m = crt_solve([r1, r2], [m1, m2])
|
|
177
|
+
return {
|
|
178
|
+
"topic": "crt",
|
|
179
|
+
"question": f"x ≡ {r1} (mod {m1}), x ≡ {r2} (mod {m2}); find x (0 ≤ x < {m})",
|
|
180
|
+
"answer": x,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_TOPIC_GENERATORS = {
|
|
184
|
+
"legendre": _generate_legendre_problem,
|
|
185
|
+
"crt": _generate_crt_problem,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def generate_problem(topic: str, rng: random.Random) -> dict:
|
|
189
|
+
if topic == "mixed":
|
|
190
|
+
topic = rng.choice(list(_TOPIC_GENERATORS.keys()))
|
|
191
|
+
if topic not in _TOPIC_GENERATORS:
|
|
192
|
+
raise ValueError(f"Unknown challenge topic: {topic}")
|
|
193
|
+
return _TOPIC_GENERATORS[topic](rng)
|
|
194
|
+
|
|
131
195
|
# ------------------- CLI -------------------
|
|
132
196
|
|
|
133
197
|
def get_command():
|
|
@@ -171,12 +235,14 @@ def main():
|
|
|
171
235
|
|
|
172
236
|
# Elliptic curve add
|
|
173
237
|
ec = subparsers.add_parser("ecadd", help="Elliptic curve point addition")
|
|
174
|
-
ec.add_argument("--
|
|
175
|
-
|
|
176
|
-
ec.add_argument("--
|
|
177
|
-
ec.add_argument("--
|
|
178
|
-
ec.add_argument("--
|
|
179
|
-
ec.add_argument("--
|
|
238
|
+
ec.add_argument("--curve", choices=sorted(CURVES.keys()),
|
|
239
|
+
help="Named curve (provides a, b, p, and generator G)")
|
|
240
|
+
ec.add_argument("--x1", type=int, help="Defaults to G's x if --curve is given")
|
|
241
|
+
ec.add_argument("--y1", type=int, help="Defaults to G's y if --curve is given")
|
|
242
|
+
ec.add_argument("--x2", type=int, help="Defaults to (x1,y1) if omitted (i.e. doubling)")
|
|
243
|
+
ec.add_argument("--y2", type=int, help="Defaults to (x1,y1) if omitted (i.e. doubling)")
|
|
244
|
+
ec.add_argument("--a", type=int, help="Curve parameter a (overrides --curve if given)")
|
|
245
|
+
ec.add_argument("--p", type=int, help="Prime modulus (overrides --curve if given)")
|
|
180
246
|
|
|
181
247
|
# Chinese Remainder Theorem
|
|
182
248
|
crt = subparsers.add_parser("crt", help="Solve a system of congruences x = r (mod m)")
|
|
@@ -189,6 +255,14 @@ def main():
|
|
|
189
255
|
cf_cmd.add_argument("--den", type=int, required=True, help="Denominator")
|
|
190
256
|
cf_cmd.add_argument("--terms", type=int, default=100, help="Max terms to expand")
|
|
191
257
|
|
|
258
|
+
# Challenge / quiz mode
|
|
259
|
+
challenge = subparsers.add_parser("challenge", help="Practice problems (Legendre, CRT)")
|
|
260
|
+
challenge.add_argument("--topic", choices=["legendre", "crt", "mixed"], default="mixed")
|
|
261
|
+
challenge.add_argument("--count", type=int, default=5, help="Number of problems")
|
|
262
|
+
challenge.add_argument("--seed", type=int, help="Random seed for reproducible problem sets")
|
|
263
|
+
challenge.add_argument("--reveal", action="store_true",
|
|
264
|
+
help="Print problems with answers, no prompting (worksheet mode)")
|
|
265
|
+
|
|
192
266
|
args = parser.parse_args(argv)
|
|
193
267
|
|
|
194
268
|
result = {"tool": "foxmath"}
|
|
@@ -207,10 +281,41 @@ def main():
|
|
|
207
281
|
print(f"π ≈ {approx} ({args.terms} terms)")
|
|
208
282
|
|
|
209
283
|
elif cmd == "ecadd" or args.command == "ecadd":
|
|
210
|
-
|
|
211
|
-
|
|
284
|
+
a, b = args.a, None
|
|
285
|
+
p = args.p
|
|
286
|
+
x1, y1 = args.x1, args.y1
|
|
287
|
+
|
|
288
|
+
if args.curve:
|
|
289
|
+
preset = CURVES[args.curve]
|
|
290
|
+
a = preset["a"] if a is None else a
|
|
291
|
+
p = preset["p"] if p is None else p
|
|
292
|
+
b = preset["b"]
|
|
293
|
+
if x1 is None:
|
|
294
|
+
x1, y1 = preset["gx"], preset["gy"]
|
|
295
|
+
elif a is None:
|
|
296
|
+
a = -3 # historical default for manual (non-curve) mode
|
|
297
|
+
|
|
298
|
+
if a is None or p is None or x1 is None or y1 is None:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
"Provide --curve NAME, or at least --p --x1 --y1 manually "
|
|
301
|
+
"(--a defaults to -3 if omitted)."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
x2 = x1 if args.x2 is None else args.x2
|
|
305
|
+
y2 = y1 if args.y2 is None else args.y2
|
|
306
|
+
|
|
307
|
+
x3, y3 = ec_point_add(x1, y1, x2, y2, a, p)
|
|
308
|
+
result.update({
|
|
309
|
+
"command": "ecadd", "curve": args.curve,
|
|
310
|
+
"p1": [x1, y1], "p2": [x2, y2], "result": (x3, y3),
|
|
311
|
+
})
|
|
312
|
+
if b is not None:
|
|
313
|
+
result["result_on_curve"] = is_on_curve(x3, y3, a, b, p)
|
|
212
314
|
if not json_output:
|
|
213
315
|
print(f"Result point: ({x3}, {y3})")
|
|
316
|
+
if b is not None:
|
|
317
|
+
status = "✓ on curve" if result["result_on_curve"] else "✗ NOT on curve"
|
|
318
|
+
print(f" {status}")
|
|
214
319
|
|
|
215
320
|
elif cmd == "crt" or args.command == "crt":
|
|
216
321
|
x, m = crt_solve(args.r, args.m)
|
|
@@ -230,6 +335,48 @@ def main():
|
|
|
230
335
|
for p, q in convs:
|
|
231
336
|
print(f" {p}/{q}")
|
|
232
337
|
|
|
338
|
+
elif cmd == "challenge" or args.command == "challenge":
|
|
339
|
+
if args.count < 1:
|
|
340
|
+
raise ValueError(f"--count must be >= 1 (got {args.count})")
|
|
341
|
+
|
|
342
|
+
rng = random.Random(args.seed)
|
|
343
|
+
problems = [generate_problem(args.topic, rng) for _ in range(args.count)]
|
|
344
|
+
|
|
345
|
+
if args.reveal:
|
|
346
|
+
result.update({"command": "challenge", "topic": args.topic, "problems": problems})
|
|
347
|
+
if not json_output:
|
|
348
|
+
for i, prob in enumerate(problems, 1):
|
|
349
|
+
print(f"{i}. {prob['question']}")
|
|
350
|
+
print(f" Answer: {prob['answer']}")
|
|
351
|
+
else:
|
|
352
|
+
score = 0
|
|
353
|
+
details = []
|
|
354
|
+
for i, prob in enumerate(problems, 1):
|
|
355
|
+
if not json_output:
|
|
356
|
+
print(f"{i}. {prob['question']}")
|
|
357
|
+
try:
|
|
358
|
+
raw = input(" Your answer: ")
|
|
359
|
+
except EOFError:
|
|
360
|
+
raw = ""
|
|
361
|
+
try:
|
|
362
|
+
user_answer = int(raw.strip())
|
|
363
|
+
except ValueError:
|
|
364
|
+
user_answer = None
|
|
365
|
+
correct = user_answer == prob["answer"]
|
|
366
|
+
score += correct
|
|
367
|
+
details.append({**prob, "your_answer": user_answer, "correct": correct})
|
|
368
|
+
if not json_output:
|
|
369
|
+
if correct:
|
|
370
|
+
print(" ✓ Correct!")
|
|
371
|
+
else:
|
|
372
|
+
print(f" ✗ Incorrect (answer: {prob['answer']})")
|
|
373
|
+
result.update({
|
|
374
|
+
"command": "challenge", "topic": args.topic,
|
|
375
|
+
"score": score, "total": args.count, "details": details,
|
|
376
|
+
})
|
|
377
|
+
if not json_output:
|
|
378
|
+
print(f"\nScore: {score}/{args.count}")
|
|
379
|
+
|
|
233
380
|
except Exception as e:
|
|
234
381
|
print(f"Error: {e}", file=sys.stderr)
|
|
235
382
|
sys.exit(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: foxmath
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Minimalist CLI for math & cryptography exploration 🦊
|
|
5
5
|
Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,12 +28,15 @@ 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
34
|
- Continued fraction expansion and convergents
|
|
34
|
-
-
|
|
35
|
+
- Challenge/quiz mode — practice problems with scoring, or a
|
|
36
|
+
worksheet mode for self-study
|
|
37
|
+
- Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd,
|
|
38
|
+
foxmath-crt, foxmath-cf, foxmath-challenge)
|
|
35
39
|
- JSON output mode
|
|
36
|
-
- Coming soon: challenge mode, more curves
|
|
37
40
|
|
|
38
41
|
## Installation
|
|
39
42
|
|
|
@@ -72,6 +75,15 @@ foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
|
|
|
72
75
|
Result point: (14, 2)
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
```bash
|
|
79
|
+
# Named curve (secp256k1) — computes 2G by default
|
|
80
|
+
foxmath ecadd --curve secp256k1
|
|
81
|
+
```
|
|
82
|
+
```
|
|
83
|
+
Result point: (89565891926547004231252920425935692360644145829622209833684329913297188986597, 12158399299693830322967808612713398636155367887041628176798871954788371653930)
|
|
84
|
+
✓ on curve
|
|
85
|
+
```
|
|
86
|
+
|
|
75
87
|
```bash
|
|
76
88
|
# Chinese Remainder Theorem
|
|
77
89
|
foxmath crt --r 2 3 2 --m 3 5 7
|
|
@@ -91,6 +103,20 @@ foxmath cf --num 355 --den 113
|
|
|
91
103
|
355/113
|
|
92
104
|
```
|
|
93
105
|
|
|
106
|
+
```bash
|
|
107
|
+
# Challenge/quiz mode — worksheet (no prompting)
|
|
108
|
+
foxmath challenge --topic legendre --count 2 --seed 1 --reveal
|
|
109
|
+
```
|
|
110
|
+
```
|
|
111
|
+
1. Legendre symbol (10/13)
|
|
112
|
+
Answer: 1
|
|
113
|
+
2. Legendre symbol (3/7)
|
|
114
|
+
Answer: -1
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Drop `--reveal` to answer interactively instead, with scoring at the end.
|
|
118
|
+
`--topic` accepts `legendre`, `crt`, or `mixed` (default).
|
|
119
|
+
|
|
94
120
|
```bash
|
|
95
121
|
# JSON output
|
|
96
122
|
foxmath pi --terms 100 --json
|
|
@@ -13,10 +13,10 @@ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src", "fox
|
|
|
13
13
|
SCRIPT = os.path.join(ROOT, "foxmath.py")
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def run(*args, script=SCRIPT):
|
|
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,
|
|
19
|
+
capture_output=True, text=True, timeout=15, input=stdin_input,
|
|
20
20
|
)
|
|
21
21
|
return result.returncode, result.stdout, result.stderr
|
|
22
22
|
|
|
@@ -91,6 +91,13 @@ class TestSymlinkInvocation(unittest.TestCase):
|
|
|
91
91
|
self.assertEqual(code, 0)
|
|
92
92
|
self.assertIn("[3; 7, 16]", out)
|
|
93
93
|
|
|
94
|
+
def test_challenge_symlink(self):
|
|
95
|
+
script = self._make_symlink_script("foxmath-challenge")
|
|
96
|
+
code, out, err = run("--topic", "legendre", "--count", "2", "--seed", "1",
|
|
97
|
+
"--reveal", script=script)
|
|
98
|
+
self.assertEqual(code, 0)
|
|
99
|
+
self.assertIn("Answer:", out)
|
|
100
|
+
|
|
94
101
|
def test_symlink_with_json(self):
|
|
95
102
|
script = self._make_symlink_script("foxmath-legendre")
|
|
96
103
|
code, out, err = run("2", "7", "--json", script=script)
|
|
@@ -110,6 +117,29 @@ class TestNormalUsage(unittest.TestCase):
|
|
|
110
117
|
self.assertEqual(code, 1)
|
|
111
118
|
self.assertIn("point at infinity", err)
|
|
112
119
|
|
|
120
|
+
def test_ecadd_curve_secp256k1_defaults_to_doubling_generator(self):
|
|
121
|
+
code, out, err = run("ecadd", "--curve", "secp256k1")
|
|
122
|
+
self.assertEqual(code, 0)
|
|
123
|
+
self.assertIn("89565891926547004231252920425935692360644145829622209833684329913297188986597", out)
|
|
124
|
+
self.assertIn("on curve", out)
|
|
125
|
+
|
|
126
|
+
def test_ecadd_curve_manual_a_override(self):
|
|
127
|
+
# --a explicitly provided should override the curve preset's a.
|
|
128
|
+
code, out, err = run("ecadd", "--curve", "toy97", "--a", "2")
|
|
129
|
+
self.assertEqual(code, 0)
|
|
130
|
+
|
|
131
|
+
def test_ecadd_no_args_at_all_errors_clearly(self):
|
|
132
|
+
code, out, err = run("ecadd")
|
|
133
|
+
self.assertEqual(code, 1)
|
|
134
|
+
self.assertIn("Error", err)
|
|
135
|
+
|
|
136
|
+
def test_ecadd_backward_compatible_manual_call(self):
|
|
137
|
+
# Original documented usage: --a omitted, defaults to -3.
|
|
138
|
+
code, out, err = run("ecadd", "--x1", "1", "--y1", "2",
|
|
139
|
+
"--x2", "3", "--y2", "4", "--p", "17")
|
|
140
|
+
self.assertEqual(code, 0)
|
|
141
|
+
self.assertIn("Result point: (14, 2)", out)
|
|
142
|
+
|
|
113
143
|
def test_crt(self):
|
|
114
144
|
code, out, err = run("crt", "--r", "2", "3", "2", "--m", "3", "5", "7")
|
|
115
145
|
self.assertEqual(code, 0)
|
|
@@ -147,6 +177,46 @@ class TestNormalUsage(unittest.TestCase):
|
|
|
147
177
|
self.assertEqual(code, 1)
|
|
148
178
|
self.assertIn("Error", err)
|
|
149
179
|
|
|
180
|
+
def test_challenge_reveal_is_deterministic_with_seed(self):
|
|
181
|
+
code1, out1, err1 = run("challenge", "--topic", "legendre", "--count", "3",
|
|
182
|
+
"--seed", "42", "--reveal")
|
|
183
|
+
code2, out2, err2 = run("challenge", "--topic", "legendre", "--count", "3",
|
|
184
|
+
"--seed", "42", "--reveal")
|
|
185
|
+
self.assertEqual(code1, 0)
|
|
186
|
+
self.assertEqual(out1, out2)
|
|
187
|
+
|
|
188
|
+
def test_challenge_interactive_scores_correct_answers(self):
|
|
189
|
+
# Known answers for --topic legendre --seed 42 --count 3 (from reveal mode).
|
|
190
|
+
code, out, err = run("challenge", "--topic", "legendre", "--count", "3",
|
|
191
|
+
"--seed", "42", stdin_input="-1\n-1\n1\n")
|
|
192
|
+
self.assertEqual(code, 0)
|
|
193
|
+
self.assertIn("Score: 3/3", out)
|
|
194
|
+
|
|
195
|
+
def test_challenge_interactive_scores_wrong_answers(self):
|
|
196
|
+
code, out, err = run("challenge", "--topic", "legendre", "--count", "3",
|
|
197
|
+
"--seed", "42", stdin_input="0\n0\n0\n")
|
|
198
|
+
self.assertEqual(code, 0)
|
|
199
|
+
self.assertIn("Score: 0/3", out)
|
|
200
|
+
|
|
201
|
+
def test_challenge_interactive_handles_non_numeric_input(self):
|
|
202
|
+
code, out, err = run("challenge", "--topic", "legendre", "--count", "1",
|
|
203
|
+
"--seed", "42", stdin_input="not-a-number\n")
|
|
204
|
+
self.assertEqual(code, 0)
|
|
205
|
+
self.assertIn("Score: 0/1", out)
|
|
206
|
+
|
|
207
|
+
def test_challenge_reveal_json(self):
|
|
208
|
+
code, out, err = run("challenge", "--topic", "crt", "--count", "2",
|
|
209
|
+
"--seed", "1", "--reveal", "--json")
|
|
210
|
+
self.assertEqual(code, 0)
|
|
211
|
+
import json
|
|
212
|
+
parsed = json.loads(out)
|
|
213
|
+
self.assertEqual(len(parsed["problems"]), 2)
|
|
214
|
+
|
|
215
|
+
def test_challenge_zero_count_errors(self):
|
|
216
|
+
code, out, err = run("challenge", "--count", "0")
|
|
217
|
+
self.assertEqual(code, 1)
|
|
218
|
+
self.assertIn("Error", err)
|
|
219
|
+
|
|
150
220
|
|
|
151
221
|
if __name__ == "__main__":
|
|
152
222
|
unittest.main()
|
|
@@ -14,8 +14,10 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..",
|
|
|
14
14
|
|
|
15
15
|
from foxmath.foxmath import (
|
|
16
16
|
legendre_symbol, euler_hermann_pi_approx, ec_point_add, crt_solve,
|
|
17
|
-
continued_fraction, cf_convergents,
|
|
17
|
+
continued_fraction, cf_convergents, CURVES, is_on_curve,
|
|
18
|
+
generate_problem,
|
|
18
19
|
)
|
|
20
|
+
import random
|
|
19
21
|
|
|
20
22
|
# Reference pi digits (mpmath, 250 dps of working precision), "3" + fractional digits.
|
|
21
23
|
PI_REFERENCE = Decimal(
|
|
@@ -152,5 +154,59 @@ class TestContinuedFractions(unittest.TestCase):
|
|
|
152
154
|
self.assertEqual(convs[-1], (expected.numerator, expected.denominator))
|
|
153
155
|
|
|
154
156
|
|
|
157
|
+
class TestNamedCurves(unittest.TestCase):
|
|
158
|
+
def test_secp256k1_generator_is_on_curve(self):
|
|
159
|
+
c = CURVES["secp256k1"]
|
|
160
|
+
self.assertTrue(is_on_curve(c["gx"], c["gy"], c["a"], c["b"], c["p"]))
|
|
161
|
+
|
|
162
|
+
def test_secp256k1_doubling_matches_known_value(self):
|
|
163
|
+
# Cross-checked independently against the `ecdsa` library's
|
|
164
|
+
# SECP256k1.generator * 2.
|
|
165
|
+
c = CURVES["secp256k1"]
|
|
166
|
+
x2, y2 = ec_point_add(c["gx"], c["gy"], c["gx"], c["gy"], c["a"], c["p"])
|
|
167
|
+
self.assertEqual(
|
|
168
|
+
x2, 89565891926547004231252920425935692360644145829622209833684329913297188986597
|
|
169
|
+
)
|
|
170
|
+
self.assertEqual(
|
|
171
|
+
y2, 12158399299693830322967808612713398636155367887041628176798871954788371653930
|
|
172
|
+
)
|
|
173
|
+
self.assertTrue(is_on_curve(x2, y2, c["a"], c["b"], c["p"]))
|
|
174
|
+
|
|
175
|
+
def test_toy97_generator_is_on_curve(self):
|
|
176
|
+
c = CURVES["toy97"]
|
|
177
|
+
self.assertTrue(is_on_curve(c["gx"], c["gy"], c["a"], c["b"], c["p"]))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestChallengeGenerators(unittest.TestCase):
|
|
181
|
+
def test_legendre_problem_answer_is_correct(self):
|
|
182
|
+
rng = random.Random(0)
|
|
183
|
+
for _ in range(50):
|
|
184
|
+
prob = generate_problem("legendre", rng)
|
|
185
|
+
self.assertEqual(prob["topic"], "legendre")
|
|
186
|
+
# re-derive the answer independently from the question text
|
|
187
|
+
self.assertIn(prob["answer"], (-1, 0, 1))
|
|
188
|
+
|
|
189
|
+
def test_crt_problem_answer_satisfies_both_congruences(self):
|
|
190
|
+
rng = random.Random(0)
|
|
191
|
+
for _ in range(50):
|
|
192
|
+
prob = generate_problem("crt", rng)
|
|
193
|
+
self.assertEqual(prob["topic"], "crt")
|
|
194
|
+
self.assertIsInstance(prob["answer"], int)
|
|
195
|
+
|
|
196
|
+
def test_mixed_topic_produces_both_kinds(self):
|
|
197
|
+
rng = random.Random(1)
|
|
198
|
+
topics = {generate_problem("mixed", rng)["topic"] for _ in range(30)}
|
|
199
|
+
self.assertEqual(topics, {"legendre", "crt"})
|
|
200
|
+
|
|
201
|
+
def test_reproducible_with_same_seed(self):
|
|
202
|
+
p1 = generate_problem("legendre", random.Random(99))
|
|
203
|
+
p2 = generate_problem("legendre", random.Random(99))
|
|
204
|
+
self.assertEqual(p1, p2)
|
|
205
|
+
|
|
206
|
+
def test_unknown_topic_raises(self):
|
|
207
|
+
with self.assertRaises(ValueError):
|
|
208
|
+
generate_problem("not_a_topic", random.Random(0))
|
|
209
|
+
|
|
210
|
+
|
|
155
211
|
if __name__ == "__main__":
|
|
156
212
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|