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,756 @@
1
+ """
2
+ numclassify/_core/divisors.py
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Divisor-based number classification functions.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from typing import List, Set
10
+
11
+ from numclassify._registry import register
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Helpers (NOT registered)
15
+ # ---------------------------------------------------------------------------
16
+
17
+ def proper_divisors(n: int) -> List[int]:
18
+ """Return all divisors of n except n itself."""
19
+ if n <= 1:
20
+ return []
21
+ divs = [1]
22
+ i = 2
23
+ while i * i <= n:
24
+ if n % i == 0:
25
+ divs.append(i)
26
+ if i != n // i:
27
+ divs.append(n // i)
28
+ i += 1
29
+ return sorted(divs)
30
+
31
+
32
+ def sigma(n: int) -> int:
33
+ """Return the sum of all divisors of n, including n itself."""
34
+ if n <= 0:
35
+ return 0
36
+ total = 0
37
+ i = 1
38
+ while i * i <= n:
39
+ if n % i == 0:
40
+ total += i
41
+ if i != n // i:
42
+ total += n // i
43
+ i += 1
44
+ return total
45
+
46
+
47
+ def num_divisors(n: int) -> int:
48
+ """Return the count of all divisors of n."""
49
+ if n <= 0:
50
+ return 0
51
+ count = 0
52
+ i = 1
53
+ while i * i <= n:
54
+ if n % i == 0:
55
+ count += 2 if i != n // i else 1
56
+ i += 1
57
+ return count
58
+
59
+
60
+ def prime_factors_set(n: int) -> Set[int]:
61
+ """Return the set of unique prime factors of n."""
62
+ factors: Set[int] = set()
63
+ if n <= 1:
64
+ return factors
65
+ while n % 2 == 0:
66
+ factors.add(2)
67
+ n //= 2
68
+ f = 3
69
+ while f * f <= n:
70
+ while n % f == 0:
71
+ factors.add(f)
72
+ n //= f
73
+ f += 2
74
+ if n > 1:
75
+ factors.add(n)
76
+ return factors
77
+
78
+
79
+ def _factorization(n: int) -> List[tuple]:
80
+ """Return list of (prime, exponent) pairs for n."""
81
+ result = []
82
+ if n <= 1:
83
+ return result
84
+ f = 2
85
+ while f * f <= n:
86
+ if n % f == 0:
87
+ exp = 0
88
+ while n % f == 0:
89
+ exp += 1
90
+ n //= f
91
+ result.append((f, exp))
92
+ f += 1
93
+ if n > 1:
94
+ result.append((n, 1))
95
+ return result
96
+
97
+
98
+ def _is_perfect_power(n: int) -> bool:
99
+ """Return True if n = k^m for some k>1, m>1."""
100
+ if n <= 1:
101
+ return False
102
+ for exp in range(2, n.bit_length() + 1):
103
+ root = round(n ** (1 / exp))
104
+ for r in (root - 1, root, root + 1):
105
+ if r > 1 and r ** exp == n:
106
+ return True
107
+ return False
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Precompute untouchable numbers up to 10000 via sieve
112
+ # ---------------------------------------------------------------------------
113
+
114
+ def _build_untouchable_set(limit: int = 10000) -> Set[int]:
115
+ """Sieve: find all values that ARE proper-divisor-sums, complement is untouchable."""
116
+ reachable: Set[int] = set()
117
+ for k in range(2, limit + 1):
118
+ s = sum(proper_divisors(k))
119
+ if s <= limit:
120
+ reachable.add(s)
121
+ # 1 is never a proper divisor sum of anything >= 2 (pd(1)=[], pd(prime)=[1])
122
+ # Actually pd(p)=[1] for prime p, so 1 is reachable. Untouchable = not reachable.
123
+ return set(range(1, limit + 1)) - reachable
124
+
125
+
126
+ _UNTOUCHABLE: Set[int] = _build_untouchable_set(10000)
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Registered classifiers
130
+ # ---------------------------------------------------------------------------
131
+
132
+ @register(name="Perfect", category="divisors", oeis="A000396",
133
+ description="Equal to the sum of its proper divisors.")
134
+ def is_perfect(n: int) -> bool:
135
+ """Return True if n equals the sum of its proper divisors.
136
+
137
+ Parameters
138
+ ----------
139
+ n : int
140
+ Positive integer.
141
+
142
+ Returns
143
+ -------
144
+ bool
145
+
146
+ Examples
147
+ --------
148
+ >>> is_perfect(6)
149
+ True
150
+ >>> is_perfect(28)
151
+ True
152
+ >>> is_perfect(12)
153
+ False
154
+ """
155
+ if n < 2:
156
+ return False
157
+ return sigma(n) - n == n
158
+
159
+
160
+ @register(name="Abundant", category="divisors", oeis="A005101",
161
+ description="Proper divisor sum exceeds n.")
162
+ def is_abundant(n: int) -> bool:
163
+ """Return True if the sum of proper divisors of n exceeds n.
164
+
165
+ Parameters
166
+ ----------
167
+ n : int
168
+
169
+ Returns
170
+ -------
171
+ bool
172
+
173
+ Examples
174
+ --------
175
+ >>> is_abundant(12)
176
+ True
177
+ >>> is_abundant(8)
178
+ False
179
+ """
180
+ if n < 1:
181
+ return False
182
+ return sigma(n) - n > n
183
+
184
+
185
+ @register(name="Deficient", category="divisors", oeis="A005100",
186
+ description="Proper divisor sum is less than n.")
187
+ def is_deficient(n: int) -> bool:
188
+ """Return True if the sum of proper divisors of n is less than n.
189
+
190
+ Parameters
191
+ ----------
192
+ n : int
193
+
194
+ Returns
195
+ -------
196
+ bool
197
+
198
+ Examples
199
+ --------
200
+ >>> is_deficient(8)
201
+ True
202
+ >>> is_deficient(6)
203
+ False
204
+ """
205
+ if n < 1:
206
+ return False
207
+ return sigma(n) - n < n
208
+
209
+
210
+ @register(name="Semiperfect", category="divisors", oeis="A005835",
211
+ description="Equal to the sum of some subset of its proper divisors.")
212
+ def is_semiperfect(n: int) -> bool:
213
+ """Return True if n equals the sum of some subset of its proper divisors.
214
+
215
+ Parameters
216
+ ----------
217
+ n : int
218
+
219
+ Returns
220
+ -------
221
+ bool
222
+ """
223
+ if n < 1:
224
+ return False
225
+ divs = proper_divisors(n)
226
+ # DP subset-sum
227
+ possible = {0}
228
+ for d in divs:
229
+ possible = possible | {x + d for x in possible}
230
+ if n in possible:
231
+ return True
232
+ return n in possible
233
+
234
+
235
+ @register(name="Weird", category="divisors", oeis="A006037",
236
+ description="Abundant but not semiperfect.")
237
+ def is_weird(n: int) -> bool:
238
+ """Return True if n is abundant but not semiperfect.
239
+
240
+ Parameters
241
+ ----------
242
+ n : int
243
+
244
+ Returns
245
+ -------
246
+ bool
247
+
248
+ Examples
249
+ --------
250
+ >>> is_weird(70)
251
+ True
252
+ """
253
+ return is_abundant(n) and not is_semiperfect(n)
254
+
255
+
256
+ @register(name="Amicable", category="divisors", oeis="A063990",
257
+ description="Pair where each number's proper divisor sum is the other.")
258
+ def is_amicable(n: int) -> bool:
259
+ """Return True if n is part of an amicable pair.
260
+
261
+ Parameters
262
+ ----------
263
+ n : int
264
+
265
+ Returns
266
+ -------
267
+ bool
268
+ """
269
+ if n < 2:
270
+ return False
271
+ m = sigma(n) - n
272
+ return m != n and sigma(m) - m == n
273
+
274
+
275
+ @register(name="Sociable", category="divisors", oeis="A003416",
276
+ description="Part of an aliquot cycle of length > 2 (checked up to length 6).")
277
+ def is_sociable(n: int) -> bool:
278
+ """Return True if n is part of a sociable cycle of length 3–6.
279
+
280
+ Parameters
281
+ ----------
282
+ n : int
283
+
284
+ Returns
285
+ -------
286
+ bool
287
+ """
288
+ if n < 2:
289
+ return False
290
+ seen = [n]
291
+ current = sigma(n) - n
292
+ for _ in range(5): # check up to length 6
293
+ if current <= 1:
294
+ return False
295
+ if current == n and len(seen) > 2:
296
+ return True
297
+ if current in seen:
298
+ return False
299
+ seen.append(current)
300
+ current = sigma(current) - current
301
+ return current == n and len(seen) > 2
302
+
303
+
304
+ @register(name="Untouchable", category="divisors", oeis="A005114",
305
+ description="No integer has n as its proper divisor sum.")
306
+ def is_untouchable(n: int) -> bool:
307
+ """Return True if no integer has n as its proper divisor sum.
308
+
309
+ Uses a precomputed sieve for values up to 10000.
310
+
311
+ Parameters
312
+ ----------
313
+ n : int
314
+
315
+ Returns
316
+ -------
317
+ bool
318
+ """
319
+ if n <= 10000:
320
+ return n in _UNTOUCHABLE
321
+ # Fallback: check a reasonable range
322
+ limit = n * 2
323
+ for k in range(2, limit + 1):
324
+ if sum(proper_divisors(k)) == n:
325
+ return False
326
+ return True
327
+
328
+
329
+ @register(name="Superperfect", category="divisors", oeis="A019279",
330
+ description="sigma(sigma(n)) == 2*n.")
331
+ def is_superperfect(n: int) -> bool:
332
+ """Return True if sigma(sigma(n)) == 2*n.
333
+
334
+ Parameters
335
+ ----------
336
+ n : int
337
+
338
+ Returns
339
+ -------
340
+ bool
341
+ """
342
+ if n < 1:
343
+ return False
344
+ return sigma(sigma(n)) == 2 * n
345
+
346
+
347
+ @register(name="Harmonic Divisor", category="divisors", oeis="A001599",
348
+ description="Harmonic mean of divisors is an integer.")
349
+ def is_harmonic_divisor(n: int) -> bool:
350
+ """Return True if the harmonic mean of n's divisors is an integer.
351
+
352
+ The harmonic mean of divisors = n * num_divisors(n) / sigma(n).
353
+
354
+ Parameters
355
+ ----------
356
+ n : int
357
+
358
+ Returns
359
+ -------
360
+ bool
361
+ """
362
+ if n < 1:
363
+ return False
364
+ nd = num_divisors(n)
365
+ s = sigma(n)
366
+ # harmonic mean = n * nd / s must be integer
367
+ return (n * nd) % s == 0
368
+
369
+
370
+ @register(name="Practical", category="divisors", oeis="A005153",
371
+ description="Every integer 1..n can be expressed as a sum of distinct divisors of n.")
372
+ def is_practical(n: int) -> bool:
373
+ """Return True if every integer 1..n is a sum of distinct divisors of n.
374
+
375
+ Uses the standard criterion: n=1, or n=2, or for each prime p^a || n
376
+ in sorted order, p <= 1 + sigma(product of earlier prime powers).
377
+
378
+ Parameters
379
+ ----------
380
+ n : int
381
+
382
+ Returns
383
+ -------
384
+ bool
385
+ """
386
+ if n == 1:
387
+ return True
388
+ if n % 2 != 0 and n != 1:
389
+ return False
390
+ factors = _factorization(n)
391
+ sigma_prefix = 1
392
+ for p, e in factors:
393
+ if p > sigma_prefix + 1:
394
+ return False
395
+ # sigma of p^e = (p^(e+1) - 1) / (p - 1)
396
+ sigma_prefix *= (p ** (e + 1) - 1) // (p - 1)
397
+ return True
398
+
399
+
400
+ @register(name="Refactorable", category="divisors", oeis="A033950",
401
+ description="Number of divisors divides n.")
402
+ def is_refactorable(n: int) -> bool:
403
+ """Return True if the number of divisors of n divides n.
404
+
405
+ Parameters
406
+ ----------
407
+ n : int
408
+
409
+ Returns
410
+ -------
411
+ bool
412
+ """
413
+ if n < 1:
414
+ return False
415
+ return n % num_divisors(n) == 0
416
+
417
+
418
+ @register(name="Highly Composite", category="divisors", oeis="A002182",
419
+ description="Has more divisors than any smaller positive integer.")
420
+ def is_highly_composite(n: int) -> bool:
421
+ """Return True if n has more divisors than any smaller positive integer.
422
+
423
+ Parameters
424
+ ----------
425
+ n : int
426
+
427
+ Returns
428
+ -------
429
+ bool
430
+ """
431
+ if n < 1:
432
+ return False
433
+ nd = num_divisors(n)
434
+ for k in range(1, n):
435
+ if num_divisors(k) >= nd:
436
+ return False
437
+ return True
438
+
439
+
440
+ @register(name="Squarefree", category="divisors", oeis="A005117",
441
+ description="Not divisible by any perfect square greater than 1.")
442
+ def is_squarefree(n: int) -> bool:
443
+ """Return True if n is not divisible by any perfect square > 1.
444
+
445
+ Parameters
446
+ ----------
447
+ n : int
448
+
449
+ Returns
450
+ -------
451
+ bool
452
+
453
+ Examples
454
+ --------
455
+ >>> is_squarefree(6)
456
+ True
457
+ >>> is_squarefree(4)
458
+ False
459
+ """
460
+ if n <= 0:
461
+ return False
462
+ if n == 1:
463
+ return True
464
+ f = 2
465
+ while f * f <= n:
466
+ if n % (f * f) == 0:
467
+ return False
468
+ f += 1
469
+ return True
470
+
471
+
472
+ @register(name="Powerful", category="divisors", oeis="A001694",
473
+ description="For every prime p dividing n, p^2 also divides n.")
474
+ def is_powerful(n: int) -> bool:
475
+ """Return True if for every prime p dividing n, p^2 also divides n.
476
+
477
+ Parameters
478
+ ----------
479
+ n : int
480
+
481
+ Returns
482
+ -------
483
+ bool
484
+
485
+ Examples
486
+ --------
487
+ >>> is_powerful(4)
488
+ True
489
+ >>> is_powerful(6)
490
+ False
491
+ """
492
+ if n <= 0:
493
+ return False
494
+ if n == 1:
495
+ return True
496
+ for p, e in _factorization(n):
497
+ if e < 2:
498
+ return False
499
+ return True
500
+
501
+
502
+ @register(name="Achilles", category="divisors", oeis="A052486",
503
+ description="Powerful but not a perfect power. Smallest: 72.")
504
+ def is_achilles(n: int) -> bool:
505
+ """Return True if n is powerful but not a perfect power.
506
+
507
+ Parameters
508
+ ----------
509
+ n : int
510
+
511
+ Returns
512
+ -------
513
+ bool
514
+
515
+ Examples
516
+ --------
517
+ >>> is_achilles(72)
518
+ True
519
+ """
520
+ return is_powerful(n) and not _is_perfect_power(n)
521
+
522
+
523
+ @register(name="Sphenic", category="divisors", oeis="A007304",
524
+ description="Product of exactly 3 distinct primes.")
525
+ def is_sphenic(n: int) -> bool:
526
+ """Return True if n is the product of exactly 3 distinct primes.
527
+
528
+ Parameters
529
+ ----------
530
+ n : int
531
+
532
+ Returns
533
+ -------
534
+ bool
535
+
536
+ Examples
537
+ --------
538
+ >>> is_sphenic(30)
539
+ True
540
+ >>> is_sphenic(12)
541
+ False
542
+ """
543
+ factors = _factorization(n)
544
+ return len(factors) == 3 and all(e == 1 for _, e in factors)
545
+
546
+
547
+ @register(name="2-smooth", category="divisors", oeis="A000079",
548
+ description="Only prime factor is 2 (powers of 2).")
549
+ def is_smooth_2(n: int) -> bool:
550
+ """Return True if the only prime factor of n is 2.
551
+
552
+ Parameters
553
+ ----------
554
+ n : int
555
+
556
+ Returns
557
+ -------
558
+ bool
559
+ """
560
+ if n < 1:
561
+ return False
562
+ return n > 0 and (n & (n - 1)) == 0
563
+
564
+
565
+ @register(name="3-smooth", category="divisors", oeis="A003586",
566
+ description="All prime factors are at most 3.")
567
+ def is_smooth_3(n: int) -> bool:
568
+ """Return True if all prime factors of n are ≤ 3.
569
+
570
+ Parameters
571
+ ----------
572
+ n : int
573
+
574
+ Returns
575
+ -------
576
+ bool
577
+ """
578
+ if n < 1:
579
+ return False
580
+ while n % 2 == 0:
581
+ n //= 2
582
+ while n % 3 == 0:
583
+ n //= 3
584
+ return n == 1
585
+
586
+
587
+ @register(name="5-smooth", category="divisors", oeis="A051037",
588
+ description="All prime factors are at most 5 (regular numbers).")
589
+ def is_smooth_5(n: int) -> bool:
590
+ """Return True if all prime factors of n are ≤ 5.
591
+
592
+ Parameters
593
+ ----------
594
+ n : int
595
+
596
+ Returns
597
+ -------
598
+ bool
599
+ """
600
+ if n < 1:
601
+ return False
602
+ for p in (2, 3, 5):
603
+ while n % p == 0:
604
+ n //= p
605
+ return n == 1
606
+
607
+
608
+ @register(name="7-smooth", category="divisors", oeis="A002473",
609
+ description="All prime factors are at most 7.")
610
+ def is_smooth_7(n: int) -> bool:
611
+ """Return True if all prime factors of n are ≤ 7.
612
+
613
+ Parameters
614
+ ----------
615
+ n : int
616
+
617
+ Returns
618
+ -------
619
+ bool
620
+ """
621
+ if n < 1:
622
+ return False
623
+ for p in (2, 3, 5, 7):
624
+ while n % p == 0:
625
+ n //= p
626
+ return n == 1
627
+
628
+
629
+ @register(name="3-rough", category="divisors",
630
+ description="No prime factor less than 3 (i.e. odd numbers > 1).")
631
+ def is_rough_3(n: int) -> bool:
632
+ """Return True if n has no prime factor less than 3 (i.e. n is odd).
633
+
634
+ Parameters
635
+ ----------
636
+ n : int
637
+
638
+ Returns
639
+ -------
640
+ bool
641
+ """
642
+ return n > 1 and n % 2 != 0
643
+
644
+
645
+ @register(name="5-rough", category="divisors",
646
+ description="No prime factor less than 5.")
647
+ def is_rough_5(n: int) -> bool:
648
+ """Return True if n has no prime factor less than 5.
649
+
650
+ Parameters
651
+ ----------
652
+ n : int
653
+
654
+ Returns
655
+ -------
656
+ bool
657
+ """
658
+ return n > 1 and n % 2 != 0 and n % 3 != 0
659
+
660
+
661
+ def _factorization_digit_count(n: int) -> int:
662
+ """Count digits used to express the prime factorization of n.
663
+
664
+ For each prime^exp: digits(prime) + (digits(exp) if exp>1 else 0).
665
+ """
666
+ if n == 1:
667
+ return 1
668
+ total = 0
669
+ for p, e in _factorization(n):
670
+ total += len(str(p))
671
+ if e > 1:
672
+ total += len(str(e))
673
+ return total
674
+
675
+
676
+ @register(name="Economical", category="divisors", oeis="A046759",
677
+ description="Prime factorization uses fewer digits than n.")
678
+ def is_economical(n: int) -> bool:
679
+ """Return True if the prime factorization of n uses fewer digits than n.
680
+
681
+ Parameters
682
+ ----------
683
+ n : int
684
+
685
+ Returns
686
+ -------
687
+ bool
688
+ """
689
+ if n < 2:
690
+ return False
691
+ return _factorization_digit_count(n) < len(str(n))
692
+
693
+
694
+ @register(name="Equidigital", category="divisors", oeis="A046758",
695
+ description="Prime factorization uses the same number of digits as n.")
696
+ def is_equidigital(n: int) -> bool:
697
+ """Return True if the prime factorization of n uses the same digits as n.
698
+
699
+ Parameters
700
+ ----------
701
+ n : int
702
+
703
+ Returns
704
+ -------
705
+ bool
706
+ """
707
+ if n < 2:
708
+ return False
709
+ return _factorization_digit_count(n) == len(str(n))
710
+
711
+
712
+ @register(name="Wasteful", category="divisors", oeis="A046760",
713
+ description="Prime factorization uses more digits than n.")
714
+ def is_wasteful(n: int) -> bool:
715
+ """Return True if the prime factorization of n uses more digits than n.
716
+
717
+ Parameters
718
+ ----------
719
+ n : int
720
+
721
+ Returns
722
+ -------
723
+ bool
724
+ """
725
+ if n < 2:
726
+ return False
727
+ return _factorization_digit_count(n) > len(str(n))
728
+
729
+
730
+ @register(name="Zumkeller", category="divisors", oeis="A083207",
731
+ description="Divisors can be partitioned into two sets with equal sum.")
732
+ def is_zumkeller(n: int) -> bool:
733
+ """Return True if the divisors of n can be split into two sets with equal sum.
734
+
735
+ Parameters
736
+ ----------
737
+ n : int
738
+
739
+ Returns
740
+ -------
741
+ bool
742
+ """
743
+ if n < 1:
744
+ return False
745
+ divs_all = proper_divisors(n) + [n]
746
+ total = sum(divs_all)
747
+ if total % 2 != 0:
748
+ return False
749
+ target = total // 2
750
+ # DP subset-sum
751
+ possible = {0}
752
+ for d in divs_all:
753
+ possible = possible | {x + d for x in possible if x + d <= target}
754
+ if target in possible:
755
+ return True
756
+ return target in possible