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.
- numclassify/__init__.py +63 -0
- numclassify/__main__.py +4 -0
- numclassify/_core/__init__.py +0 -0
- numclassify/_core/combinatorial.py +392 -0
- numclassify/_core/digital.py +403 -0
- numclassify/_core/divisors.py +756 -0
- numclassify/_core/figurate.py +357 -0
- numclassify/_core/number_theory.py +533 -0
- numclassify/_core/powers.py +349 -0
- numclassify/_core/primes.py +2100 -0
- numclassify/_core/recreational.py +245 -0
- numclassify/_core/sequences.py +488 -0
- numclassify/_registry.py +417 -0
- numclassify/cli.py +525 -0
- numclassify-0.1.0.dist-info/METADATA +220 -0
- numclassify-0.1.0.dist-info/RECORD +19 -0
- numclassify-0.1.0.dist-info/WHEEL +4 -0
- numclassify-0.1.0.dist-info/entry_points.txt +2 -0
- numclassify-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# === numclassify/_core/figurate.py ===
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Section 1: Imports
|
|
5
|
+
# =============================================================================
|
|
6
|
+
from numclassify._registry import register, REGISTRY
|
|
7
|
+
import math
|
|
8
|
+
from functools import partial
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# Section 2: Core math functions (NOT registered, used internally)
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
def _is_perfect_square(n: int) -> bool:
|
|
17
|
+
"""Integer perfect square check — no float errors."""
|
|
18
|
+
if n < 0:
|
|
19
|
+
return False
|
|
20
|
+
r = math.isqrt(n)
|
|
21
|
+
return r * r == n
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def kth_polygonal(k: int, n: int) -> int:
|
|
25
|
+
"""Return the n-th k-gonal number. Formula: n*((k-2)*n - (k-4)) // 2"""
|
|
26
|
+
return n * ((k - 2) * n - (k - 4)) // 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_k_gonal(m: int, k: int) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Check if m is a k-gonal number.
|
|
32
|
+
|
|
33
|
+
Solves P(k,n)=m via quadratic inverse. Returns True iff n is a positive integer.
|
|
34
|
+
Edge case: m<=0 returns False. m=1 returns True for all k (1 is always 1st k-gonal).
|
|
35
|
+
"""
|
|
36
|
+
if m < 1:
|
|
37
|
+
return False
|
|
38
|
+
if m == 1:
|
|
39
|
+
return True
|
|
40
|
+
# discriminant: (k-4)^2 + 8*(k-2)*m
|
|
41
|
+
# n = ((k-4) + sqrt(disc)) / (2*(k-2))
|
|
42
|
+
disc = (k - 4) ** 2 + 8 * (k - 2) * m
|
|
43
|
+
if not _is_perfect_square(disc):
|
|
44
|
+
return False
|
|
45
|
+
sqrt_disc = math.isqrt(disc)
|
|
46
|
+
numerator = (k - 4) + sqrt_disc
|
|
47
|
+
denominator = 2 * (k - 2)
|
|
48
|
+
if numerator <= 0 or numerator % denominator != 0:
|
|
49
|
+
return False
|
|
50
|
+
n = numerator // denominator
|
|
51
|
+
return n >= 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def nth_centered_kgonal(k: int, n: int) -> int:
|
|
55
|
+
"""Return the n-th centered k-gonal number. Formula: k*n*(n-1)//2 + 1"""
|
|
56
|
+
return k * n * (n - 1) // 2 + 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_centered_k_gonal(m: int, k: int) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if m is a centered k-gonal number.
|
|
62
|
+
|
|
63
|
+
Inverse: solve k*n*(n-1)/2 + 1 = m → n^2 - n - 2*(m-1)/k = 0
|
|
64
|
+
Use quadratic formula, check n is positive integer.
|
|
65
|
+
Edge case: m=1 is always True (n=0 term gives C(k,0)=1... actually n=1 gives 1 too).
|
|
66
|
+
"""
|
|
67
|
+
if m < 1:
|
|
68
|
+
return False
|
|
69
|
+
if m == 1:
|
|
70
|
+
return True
|
|
71
|
+
# k*n*(n-1)/2 + 1 = m
|
|
72
|
+
# k*n*(n-1) = 2*(m-1)
|
|
73
|
+
# k*n^2 - k*n - 2*(m-1) = 0
|
|
74
|
+
# discriminant: k^2 + 4*k*2*(m-1) = k^2 + 8*k*(m-1)
|
|
75
|
+
val = m - 1
|
|
76
|
+
if 2 * val % k != 0:
|
|
77
|
+
# Check if 2*(m-1) is divisible by k for integer n check via disc
|
|
78
|
+
pass # disc check handles this
|
|
79
|
+
disc = k * k + 8 * k * val
|
|
80
|
+
if not _is_perfect_square(disc):
|
|
81
|
+
return False
|
|
82
|
+
sqrt_disc = math.isqrt(disc)
|
|
83
|
+
numerator = k + sqrt_disc
|
|
84
|
+
denominator = 2 * k
|
|
85
|
+
if numerator <= 0 or numerator % denominator != 0:
|
|
86
|
+
return False
|
|
87
|
+
n = numerator // denominator
|
|
88
|
+
return n >= 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# =============================================================================
|
|
92
|
+
# Section 3: Polygon name table
|
|
93
|
+
# =============================================================================
|
|
94
|
+
|
|
95
|
+
POLYGON_NAMES: dict = {
|
|
96
|
+
3: ("triangular", "A000217", "A005448"),
|
|
97
|
+
4: ("square", "A000290", "A001844"),
|
|
98
|
+
5: ("pentagonal", "A000326", "A005891"),
|
|
99
|
+
6: ("hexagonal", "A000384", "A003215"),
|
|
100
|
+
7: ("heptagonal", "A000566", "A069099"),
|
|
101
|
+
8: ("octagonal", "A000567", "A016754"),
|
|
102
|
+
9: ("nonagonal", "A001106", "A060544"),
|
|
103
|
+
10: ("decagonal", "A001107", "A062786"),
|
|
104
|
+
11: ("hendecagonal", "A051682", "A069125"),
|
|
105
|
+
12: ("dodecagonal", "A051624", "A003154"),
|
|
106
|
+
13: ("tridecagonal", "A051865", ""),
|
|
107
|
+
14: ("tetradecagonal", "A051866", ""),
|
|
108
|
+
15: ("pentadecagonal", "A051867", ""),
|
|
109
|
+
16: ("hexadecagonal", "A051868", ""),
|
|
110
|
+
17: ("heptadecagonal", "A051869", ""),
|
|
111
|
+
18: ("octadecagonal", "A051870", ""),
|
|
112
|
+
19: ("enneadecagonal", "A051871", ""),
|
|
113
|
+
20: ("icosagonal", "A051872", ""),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for _k in range(21, 1001):
|
|
117
|
+
POLYGON_NAMES[_k] = (f"{_k}_gonal", "", "")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# =============================================================================
|
|
121
|
+
# Section 4: Auto-registration loop
|
|
122
|
+
# =============================================================================
|
|
123
|
+
|
|
124
|
+
for k in range(3, 1001):
|
|
125
|
+
_name, _oeis_p, _oeis_c = POLYGON_NAMES[k]
|
|
126
|
+
|
|
127
|
+
# --- Register polygonal ---
|
|
128
|
+
_func_name = f"is_{_name}"
|
|
129
|
+
_description = f"Numbers that can be arranged as a regular {_name} polygon"
|
|
130
|
+
_display_name = _name.capitalize() if not _name[0].isdigit() else _name
|
|
131
|
+
|
|
132
|
+
def _make_polygonal_func(k_=k, name_=_name):
|
|
133
|
+
def _fn(n: int) -> bool:
|
|
134
|
+
"""Return True if n is a k-gonal figurate number."""
|
|
135
|
+
return is_k_gonal(n, k_)
|
|
136
|
+
_fn.__name__ = f"is_{name_}"
|
|
137
|
+
_fn.__doc__ = f"Return True if n is a {name_} number (k={k_}-gonal)."
|
|
138
|
+
return _fn
|
|
139
|
+
|
|
140
|
+
_poly_fn = _make_polygonal_func()
|
|
141
|
+
register(
|
|
142
|
+
name=_display_name,
|
|
143
|
+
category="figurate",
|
|
144
|
+
oeis=_oeis_p,
|
|
145
|
+
description=_description,
|
|
146
|
+
aliases=[_func_name, _name],
|
|
147
|
+
)(_poly_fn)
|
|
148
|
+
|
|
149
|
+
# --- Register centered polygonal ---
|
|
150
|
+
_center_name_str = f"centered_{_name}"
|
|
151
|
+
_center_display = f"Centered {_name}"
|
|
152
|
+
|
|
153
|
+
def _make_centered_func(k_=k, name_=_name):
|
|
154
|
+
def _fn(n: int) -> bool:
|
|
155
|
+
"""Return True if n is a centered k-gonal figurate number."""
|
|
156
|
+
return is_centered_k_gonal(n, k_)
|
|
157
|
+
_fn.__name__ = f"is_centered_{name_}"
|
|
158
|
+
_fn.__doc__ = f"Return True if n is a centered {name_} number (centered k={k_}-gonal)."
|
|
159
|
+
return _fn
|
|
160
|
+
|
|
161
|
+
_cent_fn = _make_centered_func()
|
|
162
|
+
register(
|
|
163
|
+
name=_center_display,
|
|
164
|
+
category="figurate_centered",
|
|
165
|
+
oeis=_oeis_c,
|
|
166
|
+
description=f"Centered {_name} numbers",
|
|
167
|
+
aliases=[f"is_centered_{_name}", _center_name_str],
|
|
168
|
+
)(_cent_fn)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Module-level names for the 8 most common figurate types
|
|
172
|
+
is_triangular = REGISTRY["triangular"].func
|
|
173
|
+
is_square = REGISTRY["square"].func
|
|
174
|
+
is_pentagonal = REGISTRY["pentagonal"].func
|
|
175
|
+
is_hexagonal = REGISTRY["hexagonal"].func
|
|
176
|
+
is_heptagonal = REGISTRY["heptagonal"].func
|
|
177
|
+
is_octagonal = REGISTRY["octagonal"].func
|
|
178
|
+
is_centered_triangular = REGISTRY["centered_triangular"].func
|
|
179
|
+
is_centered_square = REGISTRY["centered_square"].func
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# =============================================================================
|
|
183
|
+
# Section 5: Extra figurate types
|
|
184
|
+
# =============================================================================
|
|
185
|
+
|
|
186
|
+
def is_pronic(n: int) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Return True if n is a pronic (oblong) number: n = k*(k+1) for some k >= 0.
|
|
189
|
+
|
|
190
|
+
Also known as oblong numbers or heteromecic numbers. OEIS A002378.
|
|
191
|
+
Inverse check: solve k^2 + k - n = 0, discriminant = 1 + 4*n, check perfect square.
|
|
192
|
+
"""
|
|
193
|
+
if n < 0:
|
|
194
|
+
return False
|
|
195
|
+
if n == 0:
|
|
196
|
+
return True
|
|
197
|
+
disc = 1 + 4 * n
|
|
198
|
+
if not _is_perfect_square(disc):
|
|
199
|
+
return False
|
|
200
|
+
sqrt_disc = math.isqrt(disc)
|
|
201
|
+
# k = (-1 + sqrt(1+4n)) / 2
|
|
202
|
+
numerator = -1 + sqrt_disc
|
|
203
|
+
if numerator < 0 or numerator % 2 != 0:
|
|
204
|
+
return False
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
register(
|
|
209
|
+
name="Pronic",
|
|
210
|
+
category="figurate",
|
|
211
|
+
oeis="A002378",
|
|
212
|
+
description="Pronic (oblong) numbers: k*(k+1) for non-negative integer k",
|
|
213
|
+
aliases=["is_pronic", "pronic", "oblong", "is_oblong"],
|
|
214
|
+
)(is_pronic)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_star(n: int) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Return True if n is a star number: 6*k*(k-1) + 1 for k >= 1.
|
|
220
|
+
|
|
221
|
+
Star numbers: 1, 13, 37, 73, ... OEIS A003154.
|
|
222
|
+
"""
|
|
223
|
+
if n < 1:
|
|
224
|
+
return False
|
|
225
|
+
if n == 1:
|
|
226
|
+
return True
|
|
227
|
+
# 6*k*(k-1) + 1 = n → 6k^2 - 6k + (1-n) = 0
|
|
228
|
+
# disc = 36 - 24*(1-n) = 36 + 24*(n-1) = 12 + 24n - 12 = 24n - 12 + 24 ... let me redo:
|
|
229
|
+
# 6k^2 - 6k - (n-1) = 0
|
|
230
|
+
# disc = 36 + 24*(n-1)
|
|
231
|
+
disc = 36 + 24 * (n - 1)
|
|
232
|
+
if not _is_perfect_square(disc):
|
|
233
|
+
return False
|
|
234
|
+
sqrt_disc = math.isqrt(disc)
|
|
235
|
+
numerator = 6 + sqrt_disc
|
|
236
|
+
if numerator % 12 != 0:
|
|
237
|
+
return False
|
|
238
|
+
k = numerator // 12
|
|
239
|
+
return k >= 1
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
register(
|
|
243
|
+
name="Star",
|
|
244
|
+
category="figurate",
|
|
245
|
+
oeis="A003154",
|
|
246
|
+
description="Star numbers: 6*k*(k-1) + 1",
|
|
247
|
+
aliases=["is_star", "star"],
|
|
248
|
+
)(is_star)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def is_tetrahedral(n: int) -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Return True if n is a tetrahedral number: k*(k+1)*(k+2) // 6 for k >= 1.
|
|
254
|
+
|
|
255
|
+
Tetrahedral numbers: 1, 4, 10, 20, 35, ... OEIS A000292.
|
|
256
|
+
Uses iterative check to avoid floating-point errors with cube roots.
|
|
257
|
+
"""
|
|
258
|
+
if n < 1:
|
|
259
|
+
return False
|
|
260
|
+
k = 1
|
|
261
|
+
while True:
|
|
262
|
+
t = k * (k + 1) * (k + 2) // 6
|
|
263
|
+
if t == n:
|
|
264
|
+
return True
|
|
265
|
+
if t > n:
|
|
266
|
+
return False
|
|
267
|
+
k += 1
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
register(
|
|
271
|
+
name="Tetrahedral",
|
|
272
|
+
category="figurate",
|
|
273
|
+
oeis="A000292",
|
|
274
|
+
description="Tetrahedral numbers: k*(k+1)*(k+2)/6",
|
|
275
|
+
aliases=["is_tetrahedral", "tetrahedral"],
|
|
276
|
+
)(is_tetrahedral)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def is_square_pyramidal(n: int) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Return True if n is a square pyramidal number: k*(k+1)*(2*k+1) // 6 for k >= 1.
|
|
282
|
+
|
|
283
|
+
Square pyramidal numbers: 1, 5, 14, 30, 55, ... OEIS A000330.
|
|
284
|
+
Uses iterative check.
|
|
285
|
+
"""
|
|
286
|
+
if n < 1:
|
|
287
|
+
return False
|
|
288
|
+
k = 1
|
|
289
|
+
while True:
|
|
290
|
+
t = k * (k + 1) * (2 * k + 1) // 6
|
|
291
|
+
if t == n:
|
|
292
|
+
return True
|
|
293
|
+
if t > n:
|
|
294
|
+
return False
|
|
295
|
+
k += 1
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
register(
|
|
299
|
+
name="Square Pyramidal",
|
|
300
|
+
category="figurate",
|
|
301
|
+
oeis="A000330",
|
|
302
|
+
description="Square pyramidal numbers: k*(k+1)*(2k+1)/6",
|
|
303
|
+
aliases=["is_square_pyramidal", "square_pyramidal"],
|
|
304
|
+
)(is_square_pyramidal)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def is_pentatope(n: int) -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Return True if n is a pentatope number: k*(k+1)*(k+2)*(k+3) // 24 for k >= 1.
|
|
310
|
+
|
|
311
|
+
Pentatope numbers (4-simplex numbers): 1, 5, 15, 35, 70, ... OEIS A000332.
|
|
312
|
+
Uses iterative check.
|
|
313
|
+
"""
|
|
314
|
+
if n < 1:
|
|
315
|
+
return False
|
|
316
|
+
k = 1
|
|
317
|
+
while True:
|
|
318
|
+
t = k * (k + 1) * (k + 2) * (k + 3) // 24
|
|
319
|
+
if t == n:
|
|
320
|
+
return True
|
|
321
|
+
if t > n:
|
|
322
|
+
return False
|
|
323
|
+
k += 1
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
register(
|
|
327
|
+
name="Pentatope",
|
|
328
|
+
category="figurate",
|
|
329
|
+
oeis="A000332",
|
|
330
|
+
description="Pentatope numbers (4-simplex): k*(k+1)*(k+2)*(k+3)/24",
|
|
331
|
+
aliases=["is_pentatope", "pentatope"],
|
|
332
|
+
)(is_pentatope)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# =============================================================================
|
|
336
|
+
# Section 6: Verification block
|
|
337
|
+
# =============================================================================
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
# These must all print True:
|
|
341
|
+
print(is_k_gonal(1, 3)) # 1 is triangular
|
|
342
|
+
print(is_k_gonal(10, 3)) # 10 is triangular
|
|
343
|
+
print(not is_k_gonal(11, 3)) # 11 is NOT triangular
|
|
344
|
+
print(is_k_gonal(16, 4)) # 16 is square
|
|
345
|
+
print(is_k_gonal(22, 5)) # 22 is pentagonal
|
|
346
|
+
print(is_centered_k_gonal(7, 6)) # 7 is centered hexagonal
|
|
347
|
+
print(is_triangular(21)) # True
|
|
348
|
+
print(is_square(25)) # True
|
|
349
|
+
print(is_pronic(6)) # True (2*3)
|
|
350
|
+
print(is_tetrahedral(10)) # True (k=3: 3*4*5/6=10)
|
|
351
|
+
|
|
352
|
+
# Registry size check
|
|
353
|
+
figurate_count = sum(
|
|
354
|
+
1 for v in REGISTRY.values()
|
|
355
|
+
if v.category in ("figurate", "figurate_centered")
|
|
356
|
+
)
|
|
357
|
+
print(f"Figurate types registered: {figurate_count}") # should be ~2000
|