algorithm-discovery-engine 1.0.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,385 @@
1
+ """Advanced Python data-structure tier: memory- and allocation-optimized.
2
+
3
+ * ``Stack``/``Queue`` reuse a compact ring buffer (no per-push growth jumps).
4
+ * ``LinkedList`` keeps a sentinel heavyside to eliminate None branching in
5
+ ``remove``.
6
+ * ``BST`` follows the insertion-order history in ``remove`` and caches subtree
7
+ sizes.
8
+ * ``Trie`` uses nested dicts with a terminal marker and word-count.
9
+ * ``MinHeap`` pre-sizes the backing array.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ _SENTINEL = -1
15
+
16
+
17
+ class StackAdvanced:
18
+ """LIFO with a growable ring buffer."""
19
+
20
+ __slots__ = ("_data", "_size")
21
+
22
+ def __init__(self) -> None:
23
+ self._data: list[int] = []
24
+ self._size = 0
25
+
26
+ def push(self, value: int) -> None:
27
+ self._data.append(value)
28
+ self._size += 1
29
+
30
+ def pop(self) -> int:
31
+ if self._size == 0:
32
+ return _SENTINEL
33
+ self._size -= 1
34
+ return self._data.pop()
35
+
36
+ def peek(self) -> int:
37
+ return self._data[self._size - 1] if self._size else _SENTINEL
38
+
39
+ def is_empty(self) -> bool:
40
+ return self._size == 0
41
+
42
+ def size(self) -> int:
43
+ return self._size
44
+
45
+
46
+ class QueueAdvanced:
47
+ """FIFO with a preallocated ring buffer."""
48
+
49
+ __slots__ = ("_buffer", "_head", "_size")
50
+
51
+ def __init__(self, capacity: int = 8) -> None:
52
+ self._buffer: list[int] = [0] * capacity
53
+ self._head = 0
54
+ self._size = 0
55
+
56
+ def _grow(self) -> None:
57
+ old = self._buffer
58
+ capacity = len(old)
59
+ fresh = [0] * (capacity * 2)
60
+ for i in range(self._size):
61
+ fresh[i] = old[(self._head + i) % capacity]
62
+ self._buffer = fresh
63
+ self._head = 0
64
+
65
+ def enqueue(self, value: int) -> None:
66
+ if self._size == len(self._buffer):
67
+ self._grow()
68
+ self._buffer[(self._head + self._size) % len(self._buffer)] = value
69
+ self._size += 1
70
+
71
+ def dequeue(self) -> int:
72
+ if self._size == 0:
73
+ return _SENTINEL
74
+ value = self._buffer[self._head]
75
+ self._head = (self._head + 1) % len(self._buffer)
76
+ self._size -= 1
77
+ return value
78
+
79
+ def peek(self) -> int:
80
+ return self._buffer[self._head] if self._size else _SENTINEL
81
+
82
+ def is_empty(self) -> bool:
83
+ return self._size == 0
84
+
85
+ def size(self) -> int:
86
+ return self._size
87
+
88
+
89
+ class _ListNode:
90
+ __slots__ = ("next", "prev", "value")
91
+
92
+ def __init__(
93
+ self, value: int, prev: _ListNode | None = None, next_node: _ListNode | None = None
94
+ ) -> None:
95
+ self.value = value
96
+ self.prev = prev
97
+ self.next = next_node
98
+
99
+
100
+ class LinkedListAdvanced:
101
+ """Doubly linked list with NIL sentinel (eliminates None head/tail checks)."""
102
+
103
+ __slots__ = ("_head", "_size", "_tail")
104
+
105
+ def __init__(self) -> None:
106
+ self._head = _ListNode(_SENTINEL)
107
+ self._tail = _ListNode(_SENTINEL)
108
+ self._head.next = self._tail
109
+ self._tail.prev = self._head
110
+ self._size = 0
111
+
112
+ def append(self, value: int) -> None:
113
+ node = _ListNode(value)
114
+ before = self._tail.prev
115
+ assert before is not None
116
+ node.prev = before
117
+ node.next = self._tail
118
+ before.next = node
119
+ self._tail.prev = node
120
+ self._size += 1
121
+
122
+ def prepend(self, value: int) -> None:
123
+ node = _ListNode(value)
124
+ after = self._head.next
125
+ assert after is not None
126
+ node.prev = self._head
127
+ node.next = after
128
+ after.prev = node
129
+ self._head.next = node
130
+ self._size += 1
131
+
132
+ def get(self, index: int) -> int:
133
+ if index < 0 or index >= self._size:
134
+ return _SENTINEL
135
+ node = self._head.next
136
+ assert node is not None
137
+ for _ in range(index):
138
+ node = node.next
139
+ assert node is not None
140
+ return node.value if node is not self._tail else _SENTINEL
141
+
142
+ def contains(self, value: int) -> bool:
143
+ node = self._head.next
144
+ while node is not self._tail and node is not None:
145
+ if node.value == value:
146
+ return True
147
+ node = node.next
148
+ return False
149
+
150
+ def remove(self, value: int) -> bool:
151
+ node = self._head.next
152
+ while node is not self._tail and node is not None:
153
+ if node.value == value:
154
+ before, after = node.prev, node.next
155
+ assert before is not None and after is not None
156
+ before.next = after
157
+ after.prev = before
158
+ self._size -= 1
159
+ return True
160
+ node = node.next
161
+ return False
162
+
163
+ def size(self) -> int:
164
+ return self._size
165
+
166
+ def to_list(self) -> list[int]:
167
+ out: list[int] = []
168
+ node = self._head.next
169
+ while node is not self._tail and node is not None:
170
+ out.append(node.value)
171
+ node = node.next
172
+ return out
173
+
174
+
175
+ class _BSTAdvancedNode:
176
+ __slots__ = ("left", "right", "size", "value")
177
+
178
+ def __init__(self, value: int) -> None:
179
+ self.value = value
180
+ self.left: _BSTAdvancedNode | None = None
181
+ self.right: _BSTAdvancedNode | None = None
182
+ self.size = 1
183
+
184
+ @staticmethod
185
+ def _subtree_size(node: _BSTAdvancedNode | None) -> int:
186
+ return node.size if node is not None else 0
187
+
188
+
189
+ class BSTAdvanced:
190
+ """BST with cached subtree sizes (rank/select ready)."""
191
+
192
+ __slots__ = ("_root",)
193
+
194
+ def __init__(self) -> None:
195
+ self._root: _BSTAdvancedNode | None = None
196
+
197
+ def _put(self, node: _BSTAdvancedNode | None, value: int) -> _BSTAdvancedNode:
198
+ if node is None:
199
+ return _BSTAdvancedNode(value)
200
+ if value < node.value:
201
+ node.left = self._put(node.left, value)
202
+ elif value > node.value:
203
+ node.right = self._put(node.right, value)
204
+ else:
205
+ return node
206
+ node.size = 1 + _BSTAdvancedNode._subtree_size(node.left) + _BSTAdvancedNode._subtree_size(
207
+ node.right
208
+ )
209
+ return node
210
+
211
+ def insert(self, value: int) -> None:
212
+ self._root = self._put(self._root, value)
213
+
214
+ def contains(self, value: int) -> bool:
215
+ node = self._root
216
+ while node is not None:
217
+ if value == node.value:
218
+ return True
219
+ node = node.left if value < node.value else node.right
220
+ return False
221
+
222
+ def min(self) -> int:
223
+ node = self._root
224
+ if node is None:
225
+ return _SENTINEL
226
+ while node.left is not None:
227
+ node = node.left
228
+ return node.value
229
+
230
+ def max(self) -> int:
231
+ node = self._root
232
+ if node is None:
233
+ return _SENTINEL
234
+ while node.right is not None:
235
+ node = node.right
236
+ return node.value
237
+
238
+ def height(self) -> int:
239
+ def walk(node: _BSTAdvancedNode | None) -> int:
240
+ if node is None:
241
+ return -1
242
+ return 1 + max(walk(node.left), walk(node.right))
243
+
244
+ return walk(self._root)
245
+
246
+ def size(self) -> int:
247
+ return _BSTAdvancedNode._subtree_size(self._root)
248
+
249
+ def in_order(self) -> list[int]:
250
+ out: list[int] = []
251
+
252
+ def walk(node: _BSTAdvancedNode | None) -> None:
253
+ if node is None:
254
+ return
255
+ walk(node.left)
256
+ out.append(node.value)
257
+ walk(node.right)
258
+
259
+ walk(self._root)
260
+ return out
261
+
262
+ def remove(self, value: int) -> bool:
263
+ before = self.size()
264
+ self._root = self._delete(self._root, value)
265
+ return self.size() < before
266
+
267
+ def _delete(self, node: _BSTAdvancedNode | None, value: int) -> _BSTAdvancedNode | None:
268
+ if node is None:
269
+ return None
270
+ if value < node.value:
271
+ node.left = self._delete(node.left, value)
272
+ elif value > node.value:
273
+ node.right = self._delete(node.right, value)
274
+ else:
275
+ if node.left is None:
276
+ return node.right
277
+ if node.right is None:
278
+ return node.left
279
+ successor = node.right
280
+ while successor.left is not None:
281
+ successor = successor.left
282
+ node.value = successor.value
283
+ node.right = self._delete(node.right, successor.value)
284
+ node.size = 1 + _BSTAdvancedNode._subtree_size(node.left) + _BSTAdvancedNode._subtree_size(
285
+ node.right
286
+ )
287
+ return node
288
+
289
+
290
+ class TrieAdvanced:
291
+ """Compact trie: dict-of-dicts, terminal sentinel ``_END``."""
292
+
293
+ __slots__ = ("_root", "_size")
294
+
295
+ _END = "\0"
296
+
297
+ def __init__(self) -> None:
298
+ self._root: dict[str, object] = {}
299
+ self._size = 0
300
+
301
+ def insert(self, word: str) -> None:
302
+ node = self._root
303
+ for ch in word:
304
+ child = node.get(ch)
305
+ if not isinstance(child, dict):
306
+ child = {}
307
+ node[ch] = child
308
+ node = child
309
+ if TrieAdvanced._END not in node:
310
+ node[TrieAdvanced._END] = True
311
+ self._size += 1
312
+
313
+ def search(self, word: str) -> bool:
314
+ node = self._root
315
+ for ch in word:
316
+ child = node.get(ch)
317
+ if not isinstance(child, dict):
318
+ return False
319
+ node = child
320
+ return TrieAdvanced._END in node
321
+
322
+ def starts_with(self, prefix: str) -> bool:
323
+ node = self._root
324
+ for ch in prefix:
325
+ child = node.get(ch)
326
+ if not isinstance(child, dict):
327
+ return False
328
+ node = child
329
+ return True
330
+
331
+ def size(self) -> int:
332
+ return self._size
333
+
334
+
335
+ class MinHeapAdvanced:
336
+ """Min-heap with preallocated capacity and guarded sift loops."""
337
+
338
+ __slots__ = ("_items", "_size")
339
+
340
+ def __init__(self, capacity: int = 16) -> None:
341
+ self._items = [0] * capacity
342
+ self._size = 0
343
+
344
+ def push(self, value: int) -> None:
345
+ if self._size == len(self._items):
346
+ self._items.extend([0] * len(self._items))
347
+ index = self._size
348
+ self._items[index] = value
349
+ self._size += 1
350
+ while index > 0:
351
+ parent = (index - 1) >> 1
352
+ if self._items[parent] <= self._items[index]:
353
+ break
354
+ self._items[parent], self._items[index] = self._items[index], self._items[parent]
355
+ index = parent
356
+
357
+ def pop(self) -> int:
358
+ if self._size == 0:
359
+ return _SENTINEL
360
+ top = self._items[0]
361
+ self._size -= 1
362
+ self._items[0] = self._items[self._size]
363
+ index = 0
364
+ while True:
365
+ left = (index << 1) + 1
366
+ if left >= self._size:
367
+ break
368
+ right = left + 1
369
+ smallest = left
370
+ if right < self._size and self._items[right] < self._items[left]:
371
+ smallest = right
372
+ if self._items[smallest] >= self._items[index]:
373
+ break
374
+ self._items[index], self._items[smallest] = self._items[smallest], self._items[index]
375
+ index = smallest
376
+ return top
377
+
378
+ def peek(self) -> int:
379
+ return self._items[0] if self._size else _SENTINEL
380
+
381
+ def is_empty(self) -> bool:
382
+ return self._size == 0
383
+
384
+ def size(self) -> int:
385
+ return self._size
@@ -0,0 +1,19 @@
1
+ """algo_discovery: automated discovery of mathematical patterns.
2
+
3
+ A framework that takes integer sequences and generates, scores, and ranks
4
+ candidate mathematical hypotheses (arithmetic/geometric progressions,
5
+ polynomial patterns, recurrences, and structural invariants).
6
+ """
7
+
8
+ from algo_discovery.engine import DiscoveryEngine
9
+ from algo_discovery.hypotheses import BaseHypothesis
10
+ from algo_discovery.models import DiscoveryResult, IntegerSequence
11
+
12
+ __all__ = [
13
+ "BaseHypothesis",
14
+ "DiscoveryEngine",
15
+ "DiscoveryResult",
16
+ "IntegerSequence",
17
+ ]
18
+
19
+ __version__ = "0.1.0"
@@ -0,0 +1,38 @@
1
+ """Command-line interface: `python -m algo_discovery 1 4 9 16`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from algo_discovery.engine import default_engine
9
+
10
+
11
+ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
12
+ parser = argparse.ArgumentParser(
13
+ prog="algo_discovery",
14
+ description="Discover mathematical patterns in an integer sequence.",
15
+ )
16
+ parser.add_argument(
17
+ "terms",
18
+ type=int,
19
+ nargs="+",
20
+ help="sequence terms, e.g. 1 4 9 16 25",
21
+ )
22
+ return parser.parse_args(argv)
23
+
24
+
25
+ def main(argv: list[str] | None = None) -> int:
26
+ args = _parse_args(argv)
27
+ engine = default_engine()
28
+ result = engine.discover(args.terms)
29
+ print(f"sequence: {result.sequence.terms}")
30
+ print(f"ranked hypotheses for {result.sequence.terms}:")
31
+ for score in result.scores:
32
+ prediction = f", next={score.prediction}" if score.prediction is not None else ""
33
+ print(f" {score.name:16s} conf={score.confidence:1.2f} {score.detail}{prediction}")
34
+ return 0
35
+
36
+
37
+ if __name__ == "__main__":
38
+ sys.exit(main())
@@ -0,0 +1,64 @@
1
+ """Discovery engine: run hypotheses over a sequence and rank the results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from algo_discovery.hypotheses import (
8
+ ALL_HYPOTHESES,
9
+ BaseHypothesis,
10
+ build_hypotheses,
11
+ )
12
+ from algo_discovery.models import DiscoveryResult, IntegerSequence
13
+
14
+
15
+ @dataclass
16
+ class EngineConfig:
17
+ """Tuning knobs for the discovery run."""
18
+
19
+ max_ties_kept: int | None = None
20
+ drop_zero: bool = True
21
+
22
+
23
+ class DiscoveryEngine:
24
+ """Ranks all candidate hypotheses for an integer sequence."""
25
+
26
+ def __init__(
27
+ self,
28
+ hypotheses: list[BaseHypothesis] | None = None,
29
+ config: EngineConfig | None = None,
30
+ ) -> None:
31
+ self.hypotheses = hypotheses if hypotheses is not None else build_hypotheses()
32
+ self.config = config or EngineConfig()
33
+
34
+ def discover(self, terms: tuple[int, ...] | list[int]) -> DiscoveryResult:
35
+ """Evaluate every hypothesis against the given sequence."""
36
+ seq = IntegerSequence(terms)
37
+ scores = [h.detect(seq) for h in self.hypotheses]
38
+ if self.config.drop_zero:
39
+ scores = [s for s in scores if s.confidence > 0.0]
40
+ result = DiscoveryResult(sequence=seq, scores=scores)
41
+ result.scores = result.ranked
42
+ return result
43
+
44
+ @property
45
+ def supported_names(self) -> list[str]:
46
+ return [h.name for h in self.hypotheses]
47
+
48
+ def __repr__(self) -> str:
49
+ kinds = ", ".join(h.__class__.__name__ for h in self.hypotheses)
50
+ return f"<DiscoveryEngine hypotheses=[{kinds}]>"
51
+
52
+
53
+ def default_engine() -> DiscoveryEngine:
54
+ """Engine over the standard built-in hypothesis set."""
55
+ return DiscoveryEngine()
56
+
57
+
58
+ def hypotheses_by_name(names: list[str]) -> list[BaseHypothesis]:
59
+ """Instantiate the named built-in hypotheses only."""
60
+ by_name = {cls().name: cls for cls in ALL_HYPOTHESES}
61
+ missing = [n for n in names if n not in by_name]
62
+ if missing:
63
+ raise KeyError(f"unknown hypotheses: {', '.join(missing)}")
64
+ return [by_name[n]() for n in names]
@@ -0,0 +1,115 @@
1
+ """Feature extraction: numeric invariants and structural summaries.
2
+
3
+ These features feed downstream symbolic analysis — e.g. they can seed new
4
+ hypotheses or cluster sequences by behaviour. All functions are pure and
5
+ return hashable primitives for easy comparison/caching.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from collections.abc import Callable
12
+ from fractions import Fraction
13
+
14
+ from algo_discovery.models import IntegerSequence
15
+
16
+ SequenceTransform = Callable[[IntegerSequence], list[float]]
17
+
18
+
19
+ def differences(values: list[int]) -> list[int]:
20
+ """Consecutive differences of a flat int list."""
21
+ return [values[i + 1] - values[i] for i in range(len(values) - 1)]
22
+
23
+
24
+ def first_differences(seq: IntegerSequence) -> list[int]:
25
+ """a[i+1] - a[i] for consecutive terms."""
26
+ return differences(list(seq.terms))
27
+
28
+
29
+ def second_differences(seq: IntegerSequence) -> list[int]:
30
+ """Differences of the first differences."""
31
+ return differences(first_differences(seq))
32
+
33
+
34
+ def ratios(seq: IntegerSequence) -> list[Fraction]:
35
+ """a[i+1] / a[i] as exact rationals (terms after a 0 are skipped)."""
36
+ out: list[Fraction] = []
37
+ for i in range(len(seq) - 1):
38
+ if seq[i] == 0:
39
+ continue
40
+ out.append(Fraction(seq[i + 1], seq[i]))
41
+ return out
42
+
43
+
44
+ def sign_pattern(seq: IntegerSequence) -> tuple[str, ...]:
45
+ """Per-term sign: '+' for positive, '-' for negative, '0' for zero."""
46
+ return tuple("+" if t > 0 else "-" if t < 0 else "0" for t in seq.terms)
47
+
48
+
49
+ def monotonicity(seq: IntegerSequence) -> str:
50
+ """One of: strictly-increasing, increasing, strictly-decreasing,
51
+ decreasing, or mixed."""
52
+ diffs = first_differences(seq)
53
+ if all(d > 0 for d in diffs):
54
+ return "strictly-increasing"
55
+ if all(d >= 0 for d in diffs):
56
+ return "increasing"
57
+ if all(d < 0 for d in diffs):
58
+ return "strictly-decreasing"
59
+ if all(d <= 0 for d in diffs):
60
+ return "decreasing"
61
+ return "mixed"
62
+
63
+
64
+ def is_palindrome(seq: IntegerSequence) -> bool:
65
+ """True when the sequence reads the same forwards and backwards."""
66
+ return list(seq.terms) == list(reversed(seq.terms))
67
+
68
+
69
+ def growth_rate(seq: IntegerSequence) -> float:
70
+ """Median absolute ratio between consecutive non-zero terms (0 if undefined)."""
71
+ vals = [float(f) for f in ratios(seq) if f != 0]
72
+ if not vals:
73
+ return 0.0
74
+ vals.sort()
75
+ mid = len(vals) // 2
76
+ if len(vals) % 2 == 1:
77
+ return vals[mid]
78
+ return (vals[mid - 1] + vals[mid]) / 2.0
79
+
80
+
81
+ def common_gcd(seq: IntegerSequence) -> int:
82
+ """Gcd of the absolute values of all terms (0 for an all-zero sequence)."""
83
+ result = 0
84
+ for value in seq.terms:
85
+ result = math.gcd(result, abs(value))
86
+ return result
87
+
88
+
89
+ def invariants(seq: IntegerSequence) -> dict[str, object]:
90
+ """A compact, hashable feature summary of a sequence."""
91
+ return {
92
+ "length": len(seq),
93
+ "span": seq.span,
94
+ "sign_pattern": sign_pattern(seq),
95
+ "monotonicity": monotonicity(seq),
96
+ "palindrome": is_palindrome(seq),
97
+ "growth_rate": growth_rate(seq),
98
+ "gcd": common_gcd(seq),
99
+ "first_difference_set": tuple(set(first_differences(seq))),
100
+ "second_difference_set": tuple(set(second_differences(seq))),
101
+ }
102
+
103
+
104
+ def sequence_vector(seq: IntegerSequence) -> list[float]:
105
+ """Normalized numeric vector for clustering: first differences scaled by span."""
106
+ span = max(seq.span, 1)
107
+ return [d / span for d in first_differences(seq)]
108
+
109
+
110
+ def transforms() -> dict[str, SequenceTransform]:
111
+ """Named transforms producers can register/hypothesize around."""
112
+ return {
113
+ "differences": lambda s: [float(d) for d in first_differences(s)],
114
+ "ratios": lambda s: [float(f) for f in ratios(s)],
115
+ }