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,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