numclassify 0.1.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.
@@ -0,0 +1,533 @@
1
+ """
2
+ numclassify/_core/number_theory.py
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Number-theoretic classifiers plus math utility functions.
5
+
6
+ Registered classifiers appear in the global registry.
7
+ Math utilities (euler_totient, mobius, gcd, lcm, mod_pow, mod_inv) are
8
+ NOT registered but are importable by other modules.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from typing import List
14
+
15
+ from numclassify._registry import register
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Math utilities (NOT registered)
19
+ # ---------------------------------------------------------------------------
20
+
21
+ def euler_totient(n: int) -> int:
22
+ """Return Euler's totient φ(n): count of integers 1..n coprime to n.
23
+
24
+ Parameters
25
+ ----------
26
+ n : int
27
+
28
+ Returns
29
+ -------
30
+ int
31
+ """
32
+ if n < 1:
33
+ return 0
34
+ result = n
35
+ p = 2
36
+ temp = n
37
+ while p * p <= temp:
38
+ if temp % p == 0:
39
+ while temp % p == 0:
40
+ temp //= p
41
+ result -= result // p
42
+ p += 1
43
+ if temp > 1:
44
+ result -= result // temp
45
+ return result
46
+
47
+
48
+ def mobius(n: int) -> int:
49
+ """Return the Möbius function μ(n).
50
+
51
+ Returns 0 if n has a squared prime factor, otherwise (-1)^k
52
+ where k is the number of distinct prime factors.
53
+
54
+ Parameters
55
+ ----------
56
+ n : int
57
+
58
+ Returns
59
+ -------
60
+ int
61
+ """
62
+ if n < 1:
63
+ return 0
64
+ if n == 1:
65
+ return 1
66
+ k = 0
67
+ f = 2
68
+ while f * f <= n:
69
+ if n % f == 0:
70
+ k += 1
71
+ n //= f
72
+ if n % f == 0:
73
+ return 0 # squared factor
74
+ f += 1
75
+ if n > 1:
76
+ k += 1
77
+ return (-1) ** k
78
+
79
+
80
+ def gcd(a: int, b: int) -> int:
81
+ """Return the greatest common divisor of a and b.
82
+
83
+ Parameters
84
+ ----------
85
+ a : int
86
+ b : int
87
+
88
+ Returns
89
+ -------
90
+ int
91
+ """
92
+ return math.gcd(a, b)
93
+
94
+
95
+ def lcm(a: int, b: int) -> int:
96
+ """Return the least common multiple of a and b.
97
+
98
+ Parameters
99
+ ----------
100
+ a : int
101
+ b : int
102
+
103
+ Returns
104
+ -------
105
+ int
106
+ """
107
+ if a == 0 or b == 0:
108
+ return 0
109
+ return abs(a * b) // math.gcd(a, b)
110
+
111
+
112
+ def mod_pow(base: int, exp: int, mod: int) -> int:
113
+ """Return base^exp % mod using fast modular exponentiation.
114
+
115
+ Parameters
116
+ ----------
117
+ base : int
118
+ exp : int
119
+ mod : int
120
+
121
+ Returns
122
+ -------
123
+ int
124
+ """
125
+ return pow(base, exp, mod)
126
+
127
+
128
+ def mod_inv(a: int, mod: int) -> int:
129
+ """Return the modular inverse of a mod m (requires gcd(a,m)=1).
130
+
131
+ Parameters
132
+ ----------
133
+ a : int
134
+ mod : int
135
+
136
+ Returns
137
+ -------
138
+ int
139
+ """
140
+ return pow(a, -1, mod)
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Internal helpers
145
+ # ---------------------------------------------------------------------------
146
+
147
+ def _is_prime(n: int) -> bool:
148
+ """Miller-Rabin primality (local to avoid circular import)."""
149
+ if n < 2:
150
+ return False
151
+ if n < 4:
152
+ return True
153
+ if n % 2 == 0 or n % 3 == 0:
154
+ return False
155
+ witnesses = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
156
+ d, r = n - 1, 0
157
+ while d % 2 == 0:
158
+ d //= 2
159
+ r += 1
160
+ for a in witnesses:
161
+ if a >= n:
162
+ continue
163
+ x = pow(a, d, n)
164
+ if x == 1 or x == n - 1:
165
+ continue
166
+ for _ in range(r - 1):
167
+ x = x * x % n
168
+ if x == n - 1:
169
+ break
170
+ else:
171
+ return False
172
+ return True
173
+
174
+
175
+ def _digit_sum(n: int) -> int:
176
+ return sum(int(d) for d in str(n))
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Registered classifiers
181
+ # ---------------------------------------------------------------------------
182
+
183
+ @register(name="Evil", category="number theory", oeis="A001969",
184
+ description="Has an even number of 1s in its binary representation.")
185
+ def is_evil(n: int) -> bool:
186
+ """Return True if n has an even number of 1-bits (evil number).
187
+
188
+ Parameters
189
+ ----------
190
+ n : int
191
+
192
+ Returns
193
+ -------
194
+ bool
195
+
196
+ Examples
197
+ --------
198
+ >>> is_evil(9) # 1001 -> two 1s
199
+ True
200
+ >>> is_evil(7) # 111 -> three 1s
201
+ False
202
+ """
203
+ return n >= 0 and bin(n).count("1") % 2 == 0
204
+
205
+
206
+ @register(name="Odious", category="number theory", oeis="A000069",
207
+ description="Has an odd number of 1s in its binary representation.")
208
+ def is_odious(n: int) -> bool:
209
+ """Return True if n has an odd number of 1-bits (odious number).
210
+
211
+ Parameters
212
+ ----------
213
+ n : int
214
+
215
+ Returns
216
+ -------
217
+ bool
218
+ """
219
+ return n >= 0 and bin(n).count("1") % 2 == 1
220
+
221
+
222
+ @register(name="Pernicious", category="number theory", oeis="A052294",
223
+ description="The number of 1s in binary representation is prime.")
224
+ def is_pernicious(n: int) -> bool:
225
+ """Return True if the popcount of n is a prime number.
226
+
227
+ Parameters
228
+ ----------
229
+ n : int
230
+
231
+ Returns
232
+ -------
233
+ bool
234
+ """
235
+ return n >= 0 and _is_prime(bin(n).count("1"))
236
+
237
+
238
+ @register(name="Blum Integer", category="number theory", oeis="A016105",
239
+ description="Semiprime n=p*q where both p and q are ≡ 3 mod 4.")
240
+ def is_blum_integer(n: int) -> bool:
241
+ """Return True if n is a Blum integer (semiprime p*q, p≡q≡3 mod 4, p≠q).
242
+
243
+ Parameters
244
+ ----------
245
+ n : int
246
+
247
+ Returns
248
+ -------
249
+ bool
250
+ """
251
+ if n < 15:
252
+ return False
253
+ # Find first prime factor
254
+ f = 2
255
+ while f * f <= n:
256
+ if n % f == 0:
257
+ p = f
258
+ q = n // f
259
+ # Must be semiprime: q must be prime and p != q
260
+ if q != p and _is_prime(p) and _is_prime(q):
261
+ return p % 4 == 3 and q % 4 == 3
262
+ return False
263
+ f += 1
264
+ return False
265
+
266
+
267
+ @register(name="Carmichael", category="number theory", oeis="A002997",
268
+ description="Composite n satisfying Fermat's little theorem for all coprime bases.")
269
+ def is_carmichael(n: int) -> bool:
270
+ """Return True if n is a Carmichael number (Korselt's criterion).
271
+
272
+ n is Carmichael iff: composite, squarefree, and for every prime p|n, (p-1)|(n-1).
273
+
274
+ Parameters
275
+ ----------
276
+ n : int
277
+
278
+ Returns
279
+ -------
280
+ bool
281
+
282
+ Examples
283
+ --------
284
+ >>> is_carmichael(561)
285
+ True
286
+ """
287
+ if n < 2 or _is_prime(n):
288
+ return False
289
+ # Must be squarefree
290
+ temp = n
291
+ f = 2
292
+ factors: List[int] = []
293
+ while f * f <= temp:
294
+ if temp % f == 0:
295
+ factors.append(f)
296
+ temp //= f
297
+ if temp % f == 0:
298
+ return False # not squarefree
299
+ f += 1
300
+ if temp > 1:
301
+ factors.append(temp)
302
+ # Korselt: (p-1) | (n-1) for each prime factor p
303
+ for p in factors:
304
+ if (n - 1) % (p - 1) != 0:
305
+ return False
306
+ return len(factors) >= 2
307
+
308
+
309
+ @register(name="Primary Pseudoprime", category="number theory", oeis="A001567",
310
+ description="Composite number that passes the Fermat test for base 2.")
311
+ def is_primary_pseudoprime(n: int) -> bool:
312
+ """Return True if n is a base-2 Fermat pseudoprime (composite, 2^(n-1) ≡ 1 mod n).
313
+
314
+ Parameters
315
+ ----------
316
+ n : int
317
+
318
+ Returns
319
+ -------
320
+ bool
321
+ """
322
+ if n < 4 or _is_prime(n):
323
+ return False
324
+ return pow(2, n - 1, n) == 1
325
+
326
+
327
+ @register(name="Perfect Totient", category="number theory", oeis="A082897",
328
+ description="n equals the sum of its iterated totients until reaching 1.")
329
+ def is_perfect_totient(n: int) -> bool:
330
+ """Return True if n equals the sum of iterated Euler totients until reaching 1.
331
+
332
+ Parameters
333
+ ----------
334
+ n : int
335
+
336
+ Returns
337
+ -------
338
+ bool
339
+ """
340
+ if n < 3:
341
+ return False
342
+ total = 0
343
+ k = euler_totient(n)
344
+ while k >= 1:
345
+ total += k
346
+ if total > n:
347
+ return False
348
+ if k == 1:
349
+ break
350
+ k = euler_totient(k)
351
+ return total == n
352
+
353
+
354
+ @register(name="Giuga", category="number theory", oeis="A007850",
355
+ description="Composite n where for each prime p|n, p divides (n/p - 1).")
356
+ def is_giuga(n: int) -> bool:
357
+ """Return True if n is a Giuga number.
358
+
359
+ n is Giuga iff composite and for each prime p dividing n, p | (n/p - 1).
360
+
361
+ Parameters
362
+ ----------
363
+ n : int
364
+
365
+ Returns
366
+ -------
367
+ bool
368
+ """
369
+ if n < 2 or _is_prime(n):
370
+ return False
371
+ temp = n
372
+ f = 2
373
+ while f * f <= temp:
374
+ if temp % f == 0:
375
+ if (n // f - 1) % f != 0:
376
+ return False
377
+ while temp % f == 0:
378
+ temp //= f
379
+ f += 1
380
+ if temp > 1:
381
+ if (n // temp - 1) % temp != 0:
382
+ return False
383
+ return True
384
+
385
+
386
+ @register(name="Self Number", category="number theory", oeis="A003052",
387
+ description="Cannot be expressed as k + digit_sum(k) for any k.")
388
+ def is_self_number(n: int) -> bool:
389
+ """Return True if n is a self number (Colombian number).
390
+
391
+ n is self iff no integer k satisfies k + digit_sum(k) = n.
392
+
393
+ Parameters
394
+ ----------
395
+ n : int
396
+
397
+ Returns
398
+ -------
399
+ bool
400
+
401
+ Examples
402
+ --------
403
+ >>> is_self_number(20)
404
+ True
405
+ >>> is_self_number(21)
406
+ False
407
+ """
408
+ if n < 1:
409
+ return False
410
+ # k + digit_sum(k) = n => k is in range [n - 9*len(str(n)), n)
411
+ digits_n = len(str(n))
412
+ for k in range(max(1, n - 9 * digits_n), n):
413
+ if k + _digit_sum(k) == n:
414
+ return False
415
+ return True
416
+
417
+
418
+ @register(name="Colombian", category="number theory", oeis="A003052",
419
+ description="Alias for self number: cannot be expressed as k + digit_sum(k).")
420
+ def is_colombian(n: int) -> bool:
421
+ """Return True if n is a Colombian (self) number.
422
+
423
+ Parameters
424
+ ----------
425
+ n : int
426
+
427
+ Returns
428
+ -------
429
+ bool
430
+ """
431
+ return is_self_number(n)
432
+
433
+
434
+ @register(name="Keith", category="number theory", oeis="A007629",
435
+ description="Starts with its own digits; each subsequent term is sum of previous n terms.")
436
+ def is_keith(n: int) -> bool:
437
+ """Return True if n is a Keith number.
438
+
439
+ Example: 14 → sequence 1,4,5,9,14 (each term = sum of previous 2).
440
+
441
+ Parameters
442
+ ----------
443
+ n : int
444
+
445
+ Returns
446
+ -------
447
+ bool
448
+
449
+ Examples
450
+ --------
451
+ >>> is_keith(14)
452
+ True
453
+ >>> is_keith(15)
454
+ False
455
+ """
456
+ if n < 10:
457
+ return False
458
+ digits = [int(d) for d in str(n)]
459
+ seq = list(digits)
460
+ while seq[-1] < n:
461
+ seq.append(sum(seq[-len(digits):]))
462
+ return seq[-1] == n
463
+
464
+
465
+ @register(name="Autobiographical", category="number theory", oeis="A046043",
466
+ description="Digit i counts how many times i appears in n.")
467
+ def is_autobiographical(n: int) -> bool:
468
+ """Return True if n is an autobiographical (self-describing) number.
469
+
470
+ Digit at position i (0-indexed) equals the count of digit i in n.
471
+
472
+ Parameters
473
+ ----------
474
+ n : int
475
+
476
+ Returns
477
+ -------
478
+ bool
479
+
480
+ Examples
481
+ --------
482
+ >>> is_autobiographical(1210)
483
+ True
484
+ >>> is_autobiographical(1211)
485
+ False
486
+ """
487
+ s = str(n)
488
+ k = len(s)
489
+ for i, ch in enumerate(s):
490
+ if s.count(str(i)) != int(ch):
491
+ return False
492
+ return True
493
+
494
+
495
+ @register(name="Narcissistic", category="number theory", oeis="A005188",
496
+ description="Alias for Armstrong: sum of digits each raised to number of digits equals n.")
497
+ def is_narcissistic(n: int) -> bool:
498
+ """Return True if n is a narcissistic (Armstrong) number.
499
+
500
+ Parameters
501
+ ----------
502
+ n : int
503
+
504
+ Returns
505
+ -------
506
+ bool
507
+ """
508
+ digits = str(n)
509
+ k = len(digits)
510
+ return sum(int(d) ** k for d in digits) == n
511
+
512
+
513
+ @register(name="Perfect Digital Invariant", category="number theory", oeis="A023052",
514
+ description="Sum of digits raised to some fixed power k equals n.")
515
+ def is_perfect_digital_invariant(n: int) -> bool:
516
+ """Return True if sum(d^k for d in digits(n)) == n for some k >= 1.
517
+
518
+ Parameters
519
+ ----------
520
+ n : int
521
+
522
+ Returns
523
+ -------
524
+ bool
525
+ """
526
+ if n < 1:
527
+ return False
528
+ digits = [int(d) for d in str(n)]
529
+ max_k = len(str(n)) + 3 # generous upper bound
530
+ for k in range(1, max_k + 1):
531
+ if sum(d ** k for d in digits) == n:
532
+ return True
533
+ return False