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.
- ads/__init__.py +85 -0
- ads/benchmark.py +84 -0
- ads/problems.py +190 -0
- ads/problems_advanced.py +203 -0
- ads/structures.py +370 -0
- ads/structures_advanced.py +385 -0
- algo_discovery/__init__.py +19 -0
- algo_discovery/__main__.py +38 -0
- algo_discovery/engine.py +64 -0
- algo_discovery/features.py +115 -0
- algo_discovery/hypotheses.py +251 -0
- algo_discovery/models.py +63 -0
- algorithm_discovery_engine-1.0.0.dist-info/METADATA +287 -0
- algorithm_discovery_engine-1.0.0.dist-info/RECORD +27 -0
- algorithm_discovery_engine-1.0.0.dist-info/WHEEL +4 -0
- algorithm_discovery_engine-1.0.0.dist-info/entry_points.txt +2 -0
- algorithm_discovery_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- gui/__init__.py +3 -0
- gui/__main__.py +26 -0
- gui/app.py +407 -0
- gui/core.py +149 -0
- synth/__init__.py +22 -0
- synth/__main__.py +66 -0
- synth/corpus.py +222 -0
- synth/discovery.py +198 -0
- synth/grammar.py +174 -0
- synth/search.py +432 -0
|
@@ -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())
|
algo_discovery/engine.py
ADDED
|
@@ -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
|
+
}
|