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.
synth/corpus.py ADDED
@@ -0,0 +1,222 @@
1
+ """Discovery targets, reference oracles, and fuzz generators.
2
+
3
+ Curated targets live in ``catalog/discovery_targets.json``; every target maps
4
+ to an oracle here (used as the truth source for fuzz verification) and a fuzz
5
+ input generator.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import random
12
+ from collections.abc import Callable
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ ROOT = Path(__file__).resolve().parent.parent.parent
17
+
18
+ PARSE_DELIM = "|"
19
+
20
+
21
+ def parse_input(
22
+ spec: str, arg_types: list[str] | None = None
23
+ ) -> list[Any]:
24
+ """Parse a '|'-delimited spec into typed arguments.
25
+
26
+ Without ``arg_types`` a scalar token is parsed as ``int`` and multiple
27
+ tokens become a ``list[int]``. With ``arg_types`` each segment is read per
28
+ its declared type, so a single-element list like ``"0|"`` correctly
29
+ produces ``[0]`` (a one-arg list, not the scalar ``0``).
30
+ """
31
+ if arg_types is None:
32
+ return _parse_input_legacy(spec)
33
+ raw = spec.split(PARSE_DELIM)
34
+ if raw and raw[-1] == "":
35
+ raw.pop()
36
+ parsed: list[Any] = []
37
+ for piece, atype in zip(raw, arg_types, strict=True):
38
+ if atype == "int":
39
+ parsed.append(int(piece))
40
+ else:
41
+ parsed.append([] if not piece.strip() else [int(t) for t in piece.split()])
42
+ return parsed
43
+
44
+
45
+ def _parse_input_legacy(spec: str) -> list[Any]:
46
+ """Legacy whitespace heuristic (scalar token -> int, more -> list)."""
47
+ parsed: list[Any] = []
48
+ for part in spec.split(PARSE_DELIM):
49
+ tokens = part.split()
50
+ if not tokens:
51
+ continue
52
+ if len(tokens) == 1:
53
+ token = tokens[0]
54
+ if token.lower() in {"true", "false"}:
55
+ parsed.append(token.lower() == "true")
56
+ else:
57
+ parsed.append(int(token))
58
+ else:
59
+ parsed.append([int(token) for token in tokens])
60
+ return parsed
61
+
62
+
63
+ # --- oracles --------------------------------------------------------------------
64
+
65
+
66
+ def _kadane(nums: list[int]) -> int:
67
+ best = nums[0] if nums else 0
68
+ running = 0
69
+ for value in nums:
70
+ running = max(value, running + value)
71
+ best = max(best, running)
72
+ return best
73
+
74
+
75
+ def oracle_max_circular_subarray(nums: list[int]) -> int:
76
+ """Maximum circular subarray sum (empty selection allowed -> 0)."""
77
+ if not nums:
78
+ return 0
79
+ total = sum(nums)
80
+ if all(value < 0 for value in nums):
81
+ return 0
82
+ min_sub = _kadane([-value for value in nums])
83
+ return max(_kadane(nums), total + min_sub)
84
+
85
+
86
+ def oracle_best_time_buy_sell(prices: list[int]) -> int:
87
+ """Maximum profit from one buy/sell; 0 if none possible."""
88
+ if len(prices) < 2:
89
+ return 0
90
+ best = 0
91
+ low = prices[0]
92
+ for price in prices[1:]:
93
+ low = min(low, price)
94
+ best = max(best, price - low)
95
+ return best
96
+
97
+
98
+ def oracle_jump_game(nums: list[int]) -> bool:
99
+ """Whether the last index is reachable by jumping nums[i] steps."""
100
+ reach = 0
101
+ for i, value in enumerate(nums):
102
+ if i > reach:
103
+ return False
104
+ reach = max(reach, i + value)
105
+ return True
106
+
107
+
108
+ def oracle_majority_element(nums: list[int]) -> int:
109
+ """Element appearing more than n//2 times (guaranteed in test inputs)."""
110
+ return sorted(nums)[len(nums) // 2]
111
+
112
+
113
+ def oracle_contains_duplicate(nums: list[int]) -> bool:
114
+ """Whether any value appears more than once."""
115
+ return len(nums) != len(set(nums))
116
+
117
+
118
+ def oracle_climbing_stairs(n: int) -> int:
119
+ """Ways to climb n stairs taking 1 or 2 steps."""
120
+ a, b = 1, 1
121
+ for _ in range(n):
122
+ a, b = b, a + b
123
+ return a
124
+
125
+
126
+ ORACLES: dict[str, Callable[..., Any]] = {
127
+ "max_circular_subarray": oracle_max_circular_subarray,
128
+ "best_time_buy_sell": oracle_best_time_buy_sell,
129
+ "jump_game": oracle_jump_game,
130
+ "majority_element": oracle_majority_element,
131
+ "contains_duplicate": oracle_contains_duplicate,
132
+ "climbing_stairs": oracle_climbing_stairs,
133
+ "max_subarray": _kadane,
134
+ }
135
+
136
+
137
+ # --- fuzz generators -------------------------------------------------------------
138
+
139
+ def _small_int_list(rng: random.Random, low: int, high: int) -> list[int]:
140
+ length = rng.randrange(0, 13)
141
+ return [rng.randrange(low, high + 1) for _ in range(length)]
142
+
143
+
144
+ def fuzz_int_list(rng: random.Random) -> list[int]:
145
+ return _small_int_list(rng, -12, 12)
146
+
147
+
148
+ def fuzz_prices(rng: random.Random) -> list[int]:
149
+ return _small_int_list(rng, 0, 12)
150
+
151
+
152
+ def fuzz_reach(rng: random.Random) -> list[int]:
153
+ return _small_int_list(rng, 0, 5)
154
+
155
+
156
+ def fuzz_majority(rng: random.Random) -> list[int]:
157
+ length = rng.randrange(1, 9)
158
+ token = rng.randrange(-6, 6)
159
+ majority_count = length // 2 + 1
160
+ fill = [rng.randrange(-6, 6) for _ in range(length - majority_count)]
161
+ out = [token] * majority_count + fill
162
+ rng.shuffle(out)
163
+ return out
164
+
165
+
166
+ def fuzz_small_int(rng: random.Random) -> int:
167
+ return rng.randrange(0, 16)
168
+
169
+
170
+ FUZZ: dict[str, Callable[[random.Random], Any]] = {
171
+ "max_circular_subarray": fuzz_int_list,
172
+ "best_time_buy_sell": fuzz_prices,
173
+ "jump_game": fuzz_reach,
174
+ "majority_element": fuzz_majority,
175
+ "contains_duplicate": fuzz_int_list,
176
+ "climbing_stairs": fuzz_small_int,
177
+ "max_subarray": fuzz_int_list,
178
+ }
179
+
180
+ # --- loading ----------------------------------------------------------------------
181
+
182
+ DISCOVERY_TARGETS: dict[str, dict[str, Any]] = {}
183
+
184
+
185
+ def parse_output(spec: str) -> Any:
186
+ """Parse an expected output token into bool/int."""
187
+ token = spec.strip()
188
+ if token.lower() in {"true", "false"}:
189
+ return token.lower() == "true"
190
+ return int(token)
191
+
192
+
193
+ def load_targets() -> dict[str, dict[str, Any]]:
194
+ """Load ``catalog/discovery_targets.json`` once."""
195
+ if DISCOVERY_TARGETS:
196
+ return DISCOVERY_TARGETS
197
+ path = ROOT / "catalog" / "discovery_targets.json"
198
+ payload = json.loads(path.read_text(encoding="utf-8"))
199
+ for target in payload["targets"]:
200
+ oracle_name = target.get("oracle", target["id"])
201
+ fuzz_name = target.get("fuzz", target["id"])
202
+ target["oracle"] = ORACLES[oracle_name]
203
+ target["fuzz"] = FUZZ[fuzz_name]
204
+ target["examples"] = [
205
+ (parse_input(case["in"], target.get("arg_types")), parse_output(case["out"]))
206
+ for case in target["cases"]
207
+ ]
208
+ DISCOVERY_TARGETS[target["id"]] = target
209
+ return DISCOVERY_TARGETS
210
+
211
+
212
+ def fuzz_cases(target_id: str, count: int) -> list[tuple[list[Any], Any]]:
213
+ """Draw ``count`` fuzz cases verified against the target's oracle."""
214
+ target = load_targets()[target_id]
215
+ oracle = target["oracle"]
216
+ generator = target["fuzz"]
217
+ rng = random.Random(target_id + str(count))
218
+ cases: list[tuple[list[Any], Any]] = []
219
+ for _ in range(count):
220
+ inputs = [generator(rng)]
221
+ cases.append((inputs, oracle(*inputs)))
222
+ return cases
synth/discovery.py ADDED
@@ -0,0 +1,198 @@
1
+ """Discovery orchestrator: search, verify, classify, and report.
2
+
3
+ For every target in ``catalog/discovery_targets.json``:
4
+
5
+ 1. Search for a candidate algorithm (grammar search or strategy template).
6
+ 2. Verify it on the curated examples **and** on out-of-sample fuzz inputs
7
+ whose expected outputs come from an independent reference oracle.
8
+ 3. Classify novelty (rediscovered vs. new to the solved catalog) and estimate
9
+ complexity.
10
+ 4. Persist ``catalog/discoveries/report.json``, ``report.md``, and a runnable
11
+ per-target solution module.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import time
18
+ from typing import Any
19
+
20
+ from synth import corpus, search
21
+
22
+ ROOT = corpus.ROOT
23
+ DISCOVERIES_DIR = ROOT / "catalog" / "discoveries"
24
+ SOLUTIONS_DIR = DISCOVERIES_DIR / "solutions"
25
+
26
+ FUZZ_COUNT = 60
27
+
28
+
29
+ def _solved_catalog_ids() -> set[str]:
30
+ payload = json.loads((ROOT / "catalog" / "problems.json").read_text(encoding="utf-8"))
31
+ ids = {entry["id"] for entry in payload.get("algorithms", [])}
32
+ ids.update({entry["id"] for entry in payload.get("structures", [])})
33
+ return ids
34
+
35
+
36
+ def _verify_curated(candidate: search.Candidate, target: dict[str, Any]) -> bool:
37
+ namespace: dict[str, Any] = {}
38
+ exec(compile(candidate.source, "<synth>", "exec"), namespace)
39
+ fn = namespace[candidate.function_name]
40
+ for inputs, expected in target["examples"]:
41
+ try:
42
+ if fn(*inputs) != expected:
43
+ return False
44
+ except Exception: # pragma: no cover - defensive
45
+ return False
46
+ return True
47
+
48
+
49
+ def _verify_fuzz(candidate: search.Candidate, target_id: str) -> tuple[bool, int]:
50
+ cases = corpus.fuzz_cases(target_id, FUZZ_COUNT)
51
+ namespace: dict[str, Any] = {}
52
+ exec(compile(candidate.source, "<synth>", "exec"), namespace)
53
+ fn = namespace[candidate.function_name]
54
+ for inputs, expected in cases:
55
+ try:
56
+ if fn(*inputs) != expected:
57
+ return False, -1
58
+ except Exception: # pragma: no cover - defensive
59
+ return False, -1
60
+ return True, len(cases)
61
+
62
+
63
+ def _classify(target_id: str, kind: str) -> dict[str, str]:
64
+ solved = _solved_catalog_ids()
65
+ if target_id in solved:
66
+ status = (
67
+ "rediscovered" if kind == "scan" else "rediscovered-via-template"
68
+ )
69
+ note = (
70
+ "target already solved in the shared (4-language) catalog; "
71
+ "this candidate matches the same contract all tier runners assert"
72
+ )
73
+ else:
74
+ status = "new-to-catalog"
75
+ note = (
76
+ "not part of the shared catalog yet "
77
+ "- a candidate worth porting to the language tiers"
78
+ )
79
+ return {"status": status, "note": note}
80
+
81
+
82
+ def discover(
83
+ smoke: bool = False, scan_budget: int | None = None, fuzz_count: int | None = None
84
+ ) -> dict[str, Any]:
85
+ """Run the full discovery pass over every target."""
86
+ targets = corpus.load_targets()
87
+ solved = _solved_catalog_ids()
88
+ global FUZZ_COUNT
89
+ if fuzz_count is not None:
90
+ FUZZ_COUNT = fuzz_count
91
+
92
+ entries: list[dict[str, Any]] = []
93
+ for target_id, target in targets.items():
94
+ started = time.monotonic()
95
+ budget = 200_000 if smoke else (scan_budget or 600_000)
96
+ candidate = search.discover_for_target(target, scan_budget=budget)
97
+ elapsed = time.monotonic() - started
98
+ if candidate is None:
99
+ entries.append(
100
+ {
101
+ "id": target_id,
102
+ "status": "no-candidate-found",
103
+ "kind": target["kind"],
104
+ "search_ms": round(elapsed * 1000),
105
+ }
106
+ )
107
+ continue
108
+ curated_ok = _verify_curated(candidate, target)
109
+ fuzz_ok, fuzz_run = _verify_fuzz(candidate, target_id)
110
+ classification = _classify(target_id, candidate.kind)
111
+ entries.append(
112
+ {
113
+ "id": target_id,
114
+ "status": (
115
+ "verified" if (curated_ok and fuzz_ok) else "candidate-rejected"
116
+ ),
117
+ "hidden": False,
118
+ "search_ms": round(elapsed * 1000),
119
+ "curated_examples_pass": curated_ok,
120
+ "fuzz_examples_pass": fuzz_ok,
121
+ "fuzz_run": fuzz_run,
122
+ "source": candidate.source.strip(),
123
+ "size_metric": candidate.nodes,
124
+ "kind": candidate.kind,
125
+ "strategy": candidate.params.get("strategy", ""),
126
+ "time_class": candidate.time_class,
127
+ "novelty": classification["status"],
128
+ "novelty_note": classification["note"],
129
+ "signature": target["signature"],
130
+ }
131
+ )
132
+
133
+ report: dict[str, Any] = {
134
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
135
+ "fuzz_per_target": FUZZ_COUNT,
136
+ "solved_catalog_problems": len(solved),
137
+ "targets": entries,
138
+ }
139
+ _write_report(report)
140
+ return report
141
+
142
+
143
+ def _write_report(report: dict[str, Any]) -> None:
144
+ DISCOVERIES_DIR.mkdir(parents=True, exist_ok=True)
145
+ SOLUTIONS_DIR.mkdir(parents=True, exist_ok=True)
146
+ (DISCOVERIES_DIR / "report.json").write_text(
147
+ json.dumps(report, indent=2) + "\n", encoding="utf-8"
148
+ )
149
+ for entry in report["targets"]:
150
+ if "source" in entry:
151
+ (SOLUTIONS_DIR / f"{entry['id']}.py").write_text(
152
+ entry["source"] + "\n", encoding="utf-8"
153
+ )
154
+ (DISCOVERIES_DIR / "report.md").write_text(_render_markdown(report), encoding="utf-8")
155
+
156
+
157
+ def _render_markdown(report: dict[str, Any]) -> str:
158
+ lines = [
159
+ "# Discovery report",
160
+ "",
161
+ f"Fuzz cases per target: {report['fuzz_per_target']} · "
162
+ f"solved catalog problems: {report['solved_catalog_problems']}",
163
+ "",
164
+ "| target | status | strategy | size | novelty |",
165
+ "| --- | --- | --- | ---: | --- |",
166
+ ]
167
+ for entry in report["targets"]:
168
+ lines.append(
169
+ "| {} | {} | {} | {} | {} |".format(
170
+ entry["id"],
171
+ entry["status"],
172
+ entry.get("strategy", "-"),
173
+ entry.get("size_metric", "-"),
174
+ entry.get("novelty", "-"),
175
+ )
176
+ )
177
+ lines.append("")
178
+ for entry in report["targets"]:
179
+ if "source" not in entry or entry["status"] != "verified":
180
+ continue
181
+ lines.extend(
182
+ [
183
+ f"## {entry['id']} (`{entry['kind']}`)",
184
+ "",
185
+ f"`{entry['signature']}`",
186
+ "",
187
+ f"- Complexity: {entry['time_class']}",
188
+ f"- Novelty: {entry['novelty']} — {entry['novelty_note']}",
189
+ f"- Verified on {entry['curated_examples_pass'] and 'all curated examples'}"
190
+ f" and {entry['fuzz_run']} fuzz inputs.",
191
+ "",
192
+ "```python",
193
+ entry["source"],
194
+ "```",
195
+ "",
196
+ ]
197
+ )
198
+ return "\n".join(lines) + "\n"
synth/grammar.py ADDED
@@ -0,0 +1,174 @@
1
+ """Expression grammar for synthesized algorithms.
2
+
3
+ A tiny, typed-free expression language used by the search backends. Every
4
+ expression can be evaluated over a binding environment and printed back as
5
+ Python source, so discovered candidates are always runnable and reviewable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ # Hard negative/positive sentinels far from real inputs.
14
+ NEG_INF = -(1 << 30)
15
+ POS_INF = 1 << 30
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Node:
20
+ """Immutable expression tree node.
21
+
22
+ ``op`` is either a literal value (int/str) or one of the operator names
23
+ below. ``args`` holds child nodes.
24
+ """
25
+
26
+ op: str | int
27
+ args: tuple[Node, ...] = ()
28
+
29
+
30
+ def const(value: int | str) -> Node:
31
+ return Node(value)
32
+
33
+
34
+ def var(name: str) -> Node:
35
+ return Node("var", (Node(name),))
36
+
37
+
38
+ def call(name: str, *args: Node) -> Node:
39
+ return Node(name, args)
40
+
41
+
42
+ # nodes available to a scanner program ---------------------------------------
43
+
44
+ UNARY_OPS = {"neg", "abs"}
45
+ BINARY_OPS = {"add", "sub", "mul", "fdiv", "mod", "min", "max"}
46
+ CMP_OPS = {"lt", "gt", "le", "ge", "eq"}
47
+
48
+ ALL_OPS = UNARY_OPS | BINARY_OPS | CMP_OPS
49
+
50
+ # simple canonical variables a scan understands
51
+ VARIABLES = {"i", "n", "x"}
52
+
53
+ _OPERATOR_TO_PY = {
54
+ "neg": "(-{})",
55
+ "abs": "abs({})",
56
+ "add": "({} + {})",
57
+ "sub": "({} - {})",
58
+ "mul": "({} * {})",
59
+ "fdiv": "({} // {})",
60
+ "mod": "({} % {})",
61
+ "min": "min({}, {})",
62
+ "max": "max({}, {})",
63
+ "lt": "({} < {})",
64
+ "gt": "({} > {})",
65
+ "le": "({} <= {})",
66
+ "ge": "({} >= {})",
67
+ "eq": "({} == {})",
68
+ }
69
+
70
+
71
+ def evaluate(node: Node, env: dict[str, Any]) -> Any:
72
+ """Evaluate the expression tree over ``env``.
73
+
74
+ Division by zero and mod-by-zero short-circuit to a benign neutral value
75
+ so search does not crash on degenerate candidates.
76
+ """
77
+ op = node.op
78
+ if isinstance(op, int):
79
+ return op
80
+ if op == "var":
81
+ key = node.args[0].op
82
+ assert isinstance(key, str)
83
+ return env[key]
84
+ if op == "neg":
85
+ return -evaluate(node.args[0], env)
86
+ if op == "abs":
87
+ return abs(evaluate(node.args[0], env))
88
+ if op in BINARY_OPS or op in CMP_OPS:
89
+ left = evaluate(node.args[0], env)
90
+ right = evaluate(node.args[1], env)
91
+ if op == "add":
92
+ return left + right
93
+ if op == "sub":
94
+ return left - right
95
+ if op == "mul":
96
+ return left * right
97
+ if op == "fdiv":
98
+ if right == 0:
99
+ return left
100
+ sign = -1 if (left < 0) != (right < 0) else 1
101
+ return sign * (abs(left) // abs(right))
102
+ if op == "mod":
103
+ if right == 0:
104
+ return 0
105
+ sign = 1 if right > 0 else -1
106
+ return sign * (((left % abs(right)) + abs(right)) % abs(right))
107
+ if op == "min":
108
+ return min(left, right)
109
+ if op == "max":
110
+ return max(left, right)
111
+ if op == "lt":
112
+ return left < right
113
+ if op == "gt":
114
+ return left > right
115
+ if op == "le":
116
+ return left <= right
117
+ if op == "ge":
118
+ return left >= right
119
+ return left == right
120
+ raise ValueError(f"unknown node: {node}")
121
+
122
+
123
+ def to_python(node: Node) -> str:
124
+ """Render the expression tree as Python source."""
125
+ op = node.op
126
+ if isinstance(op, int):
127
+ return repr(op)
128
+ if op == "var":
129
+ return str(node.args[0].op)
130
+ if op in UNARY_OPS:
131
+ return _OPERATOR_TO_PY[op].format(to_python(node.args[0]))
132
+ if op in BINARY_OPS or op in CMP_OPS:
133
+ return _OPERATOR_TO_PY[op].format(
134
+ to_python(node.args[0]), to_python(node.args[1])
135
+ )
136
+ raise ValueError(f"unknown node: {node}")
137
+
138
+
139
+ def size(node: Node) -> int:
140
+ """Number of nodes in the tree (a cheap complexity metric)."""
141
+ return 1 + sum(size(child) for child in node.args)
142
+
143
+
144
+ def tree_depth(node: Node) -> int:
145
+ """Tree depth (used for search budget pruning)."""
146
+ return 1 + max((tree_depth(child) for child in node.args), default=0)
147
+
148
+
149
+ def grow(depth: int, variables: tuple[str, ...], constants: tuple[int, ...]) -> list[Node]:
150
+ """Enumerate all expressions up to ``depth`` over the given leaves."""
151
+ if depth <= 1:
152
+ return [var(name) for name in variables] + [const(c) for c in constants]
153
+ results = [var(name) for name in variables] + [const(c) for c in constants]
154
+ below = grow(depth - 1, variables, constants)
155
+ for name in UNARY_OPS:
156
+ for child in below:
157
+ results.append(call(name, child))
158
+ for name in BINARY_OPS | CMP_OPS:
159
+ for left in below:
160
+ for right in below:
161
+ results.append(call(name, left, right))
162
+ return results
163
+
164
+
165
+ def dedupe_python(nodes: list[Node]) -> list[Node]:
166
+ """Drop nodes whose rendered Python source is identical (dedup + normalize)."""
167
+ seen: set[str] = set()
168
+ out: list[Node] = []
169
+ for node in nodes:
170
+ key = to_python(node)
171
+ if key not in seen:
172
+ seen.add(key)
173
+ out.append(node)
174
+ return out