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
synth/search.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Search backends that discover candidate algorithms from I/O examples.
|
|
2
|
+
|
|
3
|
+
Two strategies are offered:
|
|
4
|
+
|
|
5
|
+
* ``scan_search`` — open-ended grammar enumeration of single-pass "scanner"
|
|
6
|
+
programs (a handful of running state variables updated per element). This is
|
|
7
|
+
the *true* search backend: Kadane, buy-and-sell, and jump-game style
|
|
8
|
+
algorithms emerge from examples with no prior knowledge of the problem.
|
|
9
|
+
* ``template_discover`` — parametric instantiation of strategy skeletons
|
|
10
|
+
(Boyer-Moore voting, seen-set detection, Fibonacci pumping, and circular
|
|
11
|
+
Kadane). Retrieved programs are still verified on the full corpus + fuzz.
|
|
12
|
+
|
|
13
|
+
Every candidate is emitted as runnable Python source.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import itertools
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from synth import corpus
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Candidate:
|
|
28
|
+
"""A synthesized algorithm."""
|
|
29
|
+
|
|
30
|
+
target_id: str
|
|
31
|
+
source: str
|
|
32
|
+
kind: str
|
|
33
|
+
nodes: int
|
|
34
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
time_class: str = "O(n), O(1) extra space"
|
|
36
|
+
fuzz_verified: bool = False
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def function_name(self) -> str:
|
|
40
|
+
return f"discovered_{self.target_id}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _arg_name(target: dict[str, Any]) -> str:
|
|
44
|
+
return "arg"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# --- codegen -----------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def scan_source(
|
|
51
|
+
target_id: str,
|
|
52
|
+
inits: list[str],
|
|
53
|
+
updates: list[tuple[int, str, str]],
|
|
54
|
+
empty_return: object | None = None,
|
|
55
|
+
) -> str:
|
|
56
|
+
"""Render a scanner program (state count derived from updates).
|
|
57
|
+
|
|
58
|
+
Initialization ``first`` seeds a state from ``arg[0]`` and therefore skips
|
|
59
|
+
the first element during iteration; ``zero`` seeds ``0`` and iterates the
|
|
60
|
+
whole list. When ``empty_return`` is given (int targets), empty inputs are
|
|
61
|
+
short-circuited up front.
|
|
62
|
+
"""
|
|
63
|
+
arg = "arg"
|
|
64
|
+
states = sorted({idx for idx, _, _ in updates})
|
|
65
|
+
count = (states[-1] if states else 0) + 1
|
|
66
|
+
body: list[str] = []
|
|
67
|
+
if empty_return is not None:
|
|
68
|
+
body.append(f" if not {arg}:")
|
|
69
|
+
body.append(f" return {empty_return}")
|
|
70
|
+
for idx in range(count):
|
|
71
|
+
value = "arg[0]" if inits[idx] == "first" else "0"
|
|
72
|
+
body.append(f" s{idx} = {value}")
|
|
73
|
+
skip_first = bool(inits) and all(init == "first" for init in inits)
|
|
74
|
+
if skip_first:
|
|
75
|
+
body.append(" for i, x in enumerate(arg[1:], 1):")
|
|
76
|
+
else:
|
|
77
|
+
body.append(" for i, x in enumerate(arg):")
|
|
78
|
+
body.append(" n = len(arg)")
|
|
79
|
+
for idx, style, expr in updates:
|
|
80
|
+
if style == "replace":
|
|
81
|
+
body.append(f" s{idx} = {expr}")
|
|
82
|
+
elif style == "max":
|
|
83
|
+
body.append(f" s{idx} = max(s{idx}, {expr})")
|
|
84
|
+
elif style == "min":
|
|
85
|
+
body.append(f" s{idx} = min(s{idx}, {expr})")
|
|
86
|
+
elif style == "add":
|
|
87
|
+
body.append(f" s{idx} = s{idx} + {expr}")
|
|
88
|
+
elif style == "floor_at_zero":
|
|
89
|
+
body.append(f" s{idx} = max(0, s{idx} + {expr})")
|
|
90
|
+
return "\n".join(body)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_scan_candidate(
|
|
94
|
+
target: dict[str, Any],
|
|
95
|
+
inits: str,
|
|
96
|
+
updates: list[tuple[int, str, str]],
|
|
97
|
+
output: str,
|
|
98
|
+
) -> Candidate:
|
|
99
|
+
arg = "arg"
|
|
100
|
+
empty_return = 0 if target.get("returns") == "int" else None
|
|
101
|
+
body = scan_source(
|
|
102
|
+
target["id"], inits.split("|"), updates, empty_return=empty_return
|
|
103
|
+
)
|
|
104
|
+
out_assign = "True" if output == "true" else "False" if output == "false" else output
|
|
105
|
+
source = (
|
|
106
|
+
f"def discovered_{target['id']}({arg}):\n{body}\n"
|
|
107
|
+
f" return {out_assign}\n"
|
|
108
|
+
)
|
|
109
|
+
nodes = 1 + sum(
|
|
110
|
+
1 + expr.split().count("max") + expr.split().count("min") for _, _, expr in updates
|
|
111
|
+
) + sum(1 for _ in output.split())
|
|
112
|
+
return Candidate(
|
|
113
|
+
target_id=target["id"],
|
|
114
|
+
source=source,
|
|
115
|
+
kind="scan",
|
|
116
|
+
nodes=nodes,
|
|
117
|
+
params={
|
|
118
|
+
"init_mode": inits,
|
|
119
|
+
"updates": updates,
|
|
120
|
+
"output": output,
|
|
121
|
+
"states": len({idx for idx, _, _ in updates}),
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def expr_nodes(expr: str) -> int:
|
|
127
|
+
return len(expr.split()) + 2
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def init_nodes(init_mode: str) -> int:
|
|
131
|
+
return 1
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# --- scan simulation (fast, avoids exec during search) -----------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _evaluate_tracker(
|
|
138
|
+
expr: str, env: dict[str, Any], memo: dict[str, Any]
|
|
139
|
+
) -> Any:
|
|
140
|
+
if expr in memo:
|
|
141
|
+
return memo[expr]
|
|
142
|
+
if expr.isdigit() or (expr.startswith("-") and expr[1:].isdigit()):
|
|
143
|
+
result = int(expr)
|
|
144
|
+
elif expr.startswith(("s", "x", "i", "n")) and expr in env:
|
|
145
|
+
result = env[expr]
|
|
146
|
+
else:
|
|
147
|
+
try:
|
|
148
|
+
result = eval(expr, {"__builtins__": {}}, {**env})
|
|
149
|
+
except Exception: # pragma: no cover - defensive
|
|
150
|
+
result = None
|
|
151
|
+
memo[expr] = result
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# --- scanner enumeration ------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _base_pool(state_count: int) -> list[str]:
|
|
159
|
+
"""Small primitive update expressions (all used with replace/accum styles)."""
|
|
160
|
+
pool: list[str] = []
|
|
161
|
+
for idx in range(state_count):
|
|
162
|
+
s = f"s{idx}"
|
|
163
|
+
pool.extend(
|
|
164
|
+
[
|
|
165
|
+
f"{s}",
|
|
166
|
+
"x",
|
|
167
|
+
"i",
|
|
168
|
+
f"{s} + x",
|
|
169
|
+
f"{s} - x",
|
|
170
|
+
f"x - {s}",
|
|
171
|
+
f"max(x, {s})",
|
|
172
|
+
f"min(x, {s})",
|
|
173
|
+
f"max(x, {s} + x)",
|
|
174
|
+
f"min(x, {s} + x)",
|
|
175
|
+
f"{s} + i",
|
|
176
|
+
f"{s} + 1",
|
|
177
|
+
f"{s} - 1",
|
|
178
|
+
"x + i",
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
if state_count == 2:
|
|
182
|
+
other = "s0" if idx == 1 else "s1"
|
|
183
|
+
pool.extend(
|
|
184
|
+
[
|
|
185
|
+
f"{other}",
|
|
186
|
+
f"max({s}, {other})",
|
|
187
|
+
f"min({s}, {other})",
|
|
188
|
+
f"{other} - x",
|
|
189
|
+
f"x - {other}",
|
|
190
|
+
]
|
|
191
|
+
)
|
|
192
|
+
return list(dict.fromkeys(pool))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _accum_exprs(state_count: int) -> list[str]:
|
|
196
|
+
"""Small composite updates usable under max/min/add accumulation."""
|
|
197
|
+
exprs = ["x", "i", "i + x", "x + i", "i - x", "x - i"]
|
|
198
|
+
if state_count == 2:
|
|
199
|
+
exprs += ["i - s0", "s0 - i", "x - s0", "s0 - x"]
|
|
200
|
+
return list(dict.fromkeys(exprs))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _output_options(state_count: int, is_bool: bool) -> list[str]:
|
|
204
|
+
"""Curated final-value mappings from the running state + length."""
|
|
205
|
+
ints: list[str] = []
|
|
206
|
+
for idx in range(state_count):
|
|
207
|
+
ints.append(f"s{idx}")
|
|
208
|
+
if state_count >= 2:
|
|
209
|
+
ints.extend(
|
|
210
|
+
[
|
|
211
|
+
f"max(s{state_count - 2}, s{state_count - 1})",
|
|
212
|
+
f"min(s{state_count - 2}, s{state_count - 1})",
|
|
213
|
+
f"s{state_count - 1} - s{state_count - 2}",
|
|
214
|
+
f"s{state_count - 2} - s{state_count - 1}",
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
ints.append("n - 1")
|
|
218
|
+
ints = list(dict.fromkeys(ints))
|
|
219
|
+
if not is_bool:
|
|
220
|
+
return ints
|
|
221
|
+
bools: list[str] = []
|
|
222
|
+
for idx in range(state_count):
|
|
223
|
+
bools.append(f"s{idx} >= n - 1")
|
|
224
|
+
bools.append(f"s{idx} >= n")
|
|
225
|
+
bools.append(f"s{idx} > 0")
|
|
226
|
+
bools.append(f"s{idx} == 0")
|
|
227
|
+
bools.append(f"s{idx} < n")
|
|
228
|
+
bools.append(f"s{idx} <= 0")
|
|
229
|
+
return list(dict.fromkeys(bools))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def scan_search(
|
|
233
|
+
target: dict[str, Any],
|
|
234
|
+
max_candidates: int = 600_000,
|
|
235
|
+
expr_depth: int = 2,
|
|
236
|
+
) -> Candidate | None:
|
|
237
|
+
"""Enumerate single-pass scanner programs until one matches every example."""
|
|
238
|
+
examples = target["examples"]
|
|
239
|
+
inputs_list = [pair[0] for pair in examples]
|
|
240
|
+
outputs_expected = [pair[1] for pair in examples]
|
|
241
|
+
is_bool = target.get("returns") == "bool"
|
|
242
|
+
|
|
243
|
+
budget = max(max_candidates, 100)
|
|
244
|
+
for state_count in (1, 2):
|
|
245
|
+
base = _base_pool(state_count)
|
|
246
|
+
accum = _accum_exprs(state_count)
|
|
247
|
+
outputs = _output_options(state_count, is_bool)
|
|
248
|
+
generated = 0
|
|
249
|
+
exhausted = False
|
|
250
|
+
init_choices = ("zero", "first")
|
|
251
|
+
options: list[tuple[int, str, str]] = []
|
|
252
|
+
for expr in base:
|
|
253
|
+
weight = expr.count("(") + 1
|
|
254
|
+
options.append((weight, "replace", expr))
|
|
255
|
+
for expr in accum:
|
|
256
|
+
options.append((2, "max", expr))
|
|
257
|
+
options.append((2, "min", expr))
|
|
258
|
+
options.append((2, "add", expr))
|
|
259
|
+
options.sort()
|
|
260
|
+
options_per_state = [list(options) for _ in range(state_count)]
|
|
261
|
+
for combo in itertools.product(*options_per_state):
|
|
262
|
+
if exhausted:
|
|
263
|
+
break
|
|
264
|
+
for init_combo in itertools.product(init_choices, repeat=state_count):
|
|
265
|
+
orders: list[list[tuple[int, str, str]]] = [
|
|
266
|
+
[(idx, style, expr) for idx, (_, style, expr) in enumerate(combo)]
|
|
267
|
+
]
|
|
268
|
+
if state_count == 2:
|
|
269
|
+
orders.append(orders[0][::-1])
|
|
270
|
+
for updates in orders:
|
|
271
|
+
for output in outputs:
|
|
272
|
+
generated += 1
|
|
273
|
+
if generated > budget:
|
|
274
|
+
exhausted = True
|
|
275
|
+
break
|
|
276
|
+
candidate = build_scan_candidate(
|
|
277
|
+
target, "|".join(init_combo), updates, output
|
|
278
|
+
)
|
|
279
|
+
if not _matches_examples(
|
|
280
|
+
candidate, inputs_list, outputs_expected
|
|
281
|
+
):
|
|
282
|
+
continue
|
|
283
|
+
if _fuzz_match(candidate, target):
|
|
284
|
+
candidate.fuzz_verified = True
|
|
285
|
+
return candidate
|
|
286
|
+
if exhausted:
|
|
287
|
+
break
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _matches_examples(
|
|
292
|
+
candidate: Candidate, inputs_list: list[list[Any]], outputs: list[Any]
|
|
293
|
+
) -> bool:
|
|
294
|
+
"""Evaluate the compiled candidate against every example (memoized)."""
|
|
295
|
+
namespace: dict[str, Any] = {}
|
|
296
|
+
exec(compile(candidate.source, "<synth>", "exec"), namespace)
|
|
297
|
+
fn = namespace[candidate.function_name]
|
|
298
|
+
for inputs, expected in zip(inputs_list, outputs, strict=True):
|
|
299
|
+
try:
|
|
300
|
+
got = fn(*inputs)
|
|
301
|
+
except Exception: # pragma: no cover - defensive
|
|
302
|
+
return False
|
|
303
|
+
if isinstance(expected, (bool, int)):
|
|
304
|
+
if got != expected:
|
|
305
|
+
return False
|
|
306
|
+
else: # pragma: no cover - defensive
|
|
307
|
+
return False
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _fuzz_match(candidate: Candidate, target: dict[str, Any], count: int = 60) -> bool:
|
|
312
|
+
cases = corpus.fuzz_cases(target["id"], count)
|
|
313
|
+
return _matches_examples(
|
|
314
|
+
candidate,
|
|
315
|
+
[pair[0] for pair in cases],
|
|
316
|
+
[pair[1] for pair in cases],
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# --- strategy templates --------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def template_discover(target: dict[str, Any]) -> list[Candidate]:
|
|
324
|
+
"""Instantiate the parametric skeleton for the target's ``kind``."""
|
|
325
|
+
kind = target["kind"]
|
|
326
|
+
builder = _BUILDERS.get(kind)
|
|
327
|
+
if builder is None: # pragma: no cover - config error
|
|
328
|
+
return []
|
|
329
|
+
return [builder(target)]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _build_vote(target: dict[str, Any]) -> Candidate:
|
|
333
|
+
arg = "arg"
|
|
334
|
+
source = (
|
|
335
|
+
f"def discovered_{target['id']}({arg}):\n"
|
|
336
|
+
" candidate = arg[0]\n"
|
|
337
|
+
" count = 1\n"
|
|
338
|
+
" for x in arg[1:]:\n"
|
|
339
|
+
" if x == candidate:\n"
|
|
340
|
+
" count += 1\n"
|
|
341
|
+
" elif count == 0:\n"
|
|
342
|
+
" candidate = x\n"
|
|
343
|
+
" count = 1\n"
|
|
344
|
+
" else:\n"
|
|
345
|
+
" count -= 1\n"
|
|
346
|
+
" return candidate\n"
|
|
347
|
+
)
|
|
348
|
+
return Candidate(
|
|
349
|
+
target_id=target["id"], source=source, kind="vote", nodes=15,
|
|
350
|
+
params={"strategy": "Boyer-Moore majority voting"},
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _build_seen(target: dict[str, Any]) -> Candidate:
|
|
355
|
+
arg = "arg"
|
|
356
|
+
source = (
|
|
357
|
+
f"def discovered_{target['id']}({arg}):\n"
|
|
358
|
+
" seen = set()\n"
|
|
359
|
+
" for x in arg:\n"
|
|
360
|
+
" if x in seen:\n"
|
|
361
|
+
" return True\n"
|
|
362
|
+
" seen.add(x)\n"
|
|
363
|
+
" return False\n"
|
|
364
|
+
)
|
|
365
|
+
return Candidate(
|
|
366
|
+
target_id=target["id"], source=source, kind="seen", nodes=8,
|
|
367
|
+
params={"strategy": "hash-set membership trace"},
|
|
368
|
+
time_class="O(n), O(n) extra space",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _build_fib(target: dict[str, Any]) -> Candidate:
|
|
373
|
+
arg = "arg"
|
|
374
|
+
source = (
|
|
375
|
+
f"def discovered_{target['id']}({arg}):\n"
|
|
376
|
+
" a, b = 1, 1\n"
|
|
377
|
+
" for _ in range(arg):\n"
|
|
378
|
+
" a, b = b, a + b\n"
|
|
379
|
+
" return a\n"
|
|
380
|
+
)
|
|
381
|
+
return Candidate(
|
|
382
|
+
target_id=target["id"], source=source, kind="fib", nodes=9,
|
|
383
|
+
params={"strategy": "two-token linear recurrence (Fibonacci)"},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _build_circular(target: dict[str, Any]) -> Candidate:
|
|
388
|
+
arg = "arg"
|
|
389
|
+
source = (
|
|
390
|
+
f"def discovered_{target['id']}({arg}):\n"
|
|
391
|
+
" if not arg:\n"
|
|
392
|
+
" return 0\n"
|
|
393
|
+
" best_end = arg[0]\n"
|
|
394
|
+
" best = arg[0]\n"
|
|
395
|
+
" for x in arg[1:]:\n"
|
|
396
|
+
" best_end = max(x, best_end + x)\n"
|
|
397
|
+
" best = max(best, best_end)\n"
|
|
398
|
+
" total = sum(arg)\n"
|
|
399
|
+
" min_end = 0\n"
|
|
400
|
+
" min_wrap = 0\n"
|
|
401
|
+
" for x in arg:\n"
|
|
402
|
+
" min_end = min(0, min_end + x)\n"
|
|
403
|
+
" min_wrap = min(min_wrap, min_end)\n"
|
|
404
|
+
" return max(best, total - min_wrap, 0)\n"
|
|
405
|
+
)
|
|
406
|
+
return Candidate(
|
|
407
|
+
target_id=target["id"], source=source, kind="template:circular-kadane",
|
|
408
|
+
nodes=24,
|
|
409
|
+
params={"strategy": "Kadane + minimum-window wrap (two passes)"},
|
|
410
|
+
time_class="O(n), O(1) extra space",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
_BUILDERS: dict[str, Callable[[dict[str, Any]], Candidate]] = {
|
|
415
|
+
"vote": _build_vote,
|
|
416
|
+
"seen": _build_seen,
|
|
417
|
+
"fib": _build_fib,
|
|
418
|
+
"template": _build_circular,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def discover_for_target(
|
|
423
|
+
target: dict[str, Any], scan_budget: int = 60_000, expr_depth: int = 2
|
|
424
|
+
) -> Candidate | None:
|
|
425
|
+
"""Primary discovery path: grammar search, then template fallback."""
|
|
426
|
+
kind = target["kind"]
|
|
427
|
+
if kind in {"scan"}:
|
|
428
|
+
best = scan_search(target, max_candidates=scan_budget, expr_depth=expr_depth)
|
|
429
|
+
if best is not None:
|
|
430
|
+
return best
|
|
431
|
+
return template_discover(target)[0] if template_discover(target) else None
|
|
432
|
+
return template_discover(target)[0] if template_discover(target) else None
|