gdk9-cli 0.3.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.
- examples/01_analyze.py +28 -0
- examples/02_profile.py +24 -0
- examples/03_assign.py +19 -0
- examples/04_dcg.py +41 -0
- examples/05_tokenize.py +22 -0
- examples/06_synthesize.py +20 -0
- examples/07_api.py +57 -0
- examples/08_kernel.py +35 -0
- examples/09_kernel_search_walkthrough.py +102 -0
- examples/10_conserve_vs_naive.py +100 -0
- gdk9/__init__.py +5 -0
- gdk9/ansi.py +57 -0
- gdk9/cli.py +1186 -0
- gdk9/crdt.py +88 -0
- gdk9/crypto.py +132 -0
- gdk9/data/ninefold.json +40 -0
- gdk9/data/official.example.json +16 -0
- gdk9/data/official.json +132 -0
- gdk9/data/subs.json +436 -0
- gdk9/dcg.py +356 -0
- gdk9/egglog_bridge/__init__.py +6 -0
- gdk9/egglog_bridge/dr_egglog.py +107 -0
- gdk9/egglog_bridge/fallback.py +34 -0
- gdk9/energy.py +211 -0
- gdk9/errors.py +15 -0
- gdk9/fmt.py +212 -0
- gdk9/imply.py +83 -0
- gdk9/io_utils.py +24 -0
- gdk9/kernel/__init__.py +24 -0
- gdk9/kernel/engine.py +123 -0
- gdk9/kernel/errors.py +17 -0
- gdk9/kernel/expression.py +43 -0
- gdk9/kernel/principle.py +43 -0
- gdk9/kernel/proof.py +24 -0
- gdk9/kernel/rule.py +30 -0
- gdk9/kernel/symbol.py +19 -0
- gdk9/kernel_cli.py +195 -0
- gdk9/log.py +30 -0
- gdk9/optimize.py +288 -0
- gdk9/parser.py +35 -0
- gdk9/plugins/__init__.py +9 -0
- gdk9/plugins/loader.py +346 -0
- gdk9/plugins/registry.py +42 -0
- gdk9/principles.py +131 -0
- gdk9/saturate.py +89 -0
- gdk9/state.py +138 -0
- gdk9/subs.py +108 -0
- gdk9/tokenize.py +386 -0
- gdk9/tui.py +115 -0
- gdk9/utilization.py +15 -0
- gdk9_cli-0.3.0.dist-info/METADATA +295 -0
- gdk9_cli-0.3.0.dist-info/RECORD +82 -0
- gdk9_cli-0.3.0.dist-info/WHEEL +5 -0
- gdk9_cli-0.3.0.dist-info/entry_points.txt +3 -0
- gdk9_cli-0.3.0.dist-info/licenses/LICENSE +661 -0
- gdk9_cli-0.3.0.dist-info/top_level.txt +4 -0
- scripts/batch_analyze.py +109 -0
- scripts/export_json.py +125 -0
- scripts/make_zip.py +57 -0
- scripts/profile_compare.py +165 -0
- scripts/watch_file.py +124 -0
- tests/conftest.py +26 -0
- tests/experiment/__init__.py +1 -0
- tests/experiment/test_conserve_vs_naive.py +287 -0
- tests/kernel/test_kernel_energy.py +18 -0
- tests/kernel/test_kernel_rules.py +58 -0
- tests/kernel/test_kernel_search_bound.py +34 -0
- tests/test_crypto.py +36 -0
- tests/test_crypto_secure.py +26 -0
- tests/test_dcg.py +318 -0
- tests/test_dcg_golden.py +67 -0
- tests/test_egglog_bridge.py +41 -0
- tests/test_energy.py +34 -0
- tests/test_kernel_cli_handbook.py +101 -0
- tests/test_optimize.py +21 -0
- tests/test_plugins.py +64 -0
- tests/test_rules.py +43 -0
- tests/test_rules_commit.py +40 -0
- tests/test_rules_reversibility.py +50 -0
- tests/test_state_crdt.py +87 -0
- tests/test_tokenize.py +38 -0
- tests/test_tokenize_metrics.py +33 -0
gdk9/dcg.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Directed Cognition Graph (DCG) for GDk9.
|
|
2
|
+
|
|
3
|
+
Nodes: all 52 alphabetic characters (A–Z, a–z), each labelled with its
|
|
4
|
+
symmetry class, SymPhi rest-energy, and 4D vector.
|
|
5
|
+
|
|
6
|
+
Edge types and costs
|
|
7
|
+
--------------------
|
|
8
|
+
Uppercase ↔ lowercase DC ↔ AC form conversion cost 1 (→) / 2 (←)
|
|
9
|
+
Within-class all-pairs same symmetry class, same case cost 1
|
|
10
|
+
Cross-class cycle asym → biph → idemp → invol
|
|
11
|
+
→ asym (both cases) cost 2
|
|
12
|
+
|
|
13
|
+
The cross-class cycle formalises the energy-flow circuit from the whitepaper.
|
|
14
|
+
F → E (asymmetric → biphasic) is the canonical DC→AC choice-point.
|
|
15
|
+
|
|
16
|
+
Energy model
|
|
17
|
+
------------
|
|
18
|
+
SymPhi rest-energy uses positional algebra (not digital root):
|
|
19
|
+
idempotent E = pos (1–26)
|
|
20
|
+
biphasic E = sin(pos)
|
|
21
|
+
involutive E = 1 / pos
|
|
22
|
+
asymmetric E = pos + 1
|
|
23
|
+
|
|
24
|
+
4D vector per character: [pos, type_id, √|E|, sin(pos·π/26)]
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import heapq
|
|
29
|
+
import math
|
|
30
|
+
from typing import Dict, List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
# ── Symmetry sets (whitepaper §2.2) ──────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
_IDEMP = frozenset('AHIMOTUVWXY') # idempotent: x²=x (11 letters)
|
|
35
|
+
_BIPH = frozenset('BCDEK') # biphasic: x²=f(x) (5)
|
|
36
|
+
_INVOL = frozenset('NSZ') # involutive: x²=1 (3)
|
|
37
|
+
_ASYM = frozenset('FGJLPQR') # asymmetric: x²≠x,1 (7)
|
|
38
|
+
|
|
39
|
+
_CLASS_UPPER: Dict[str, List[str]] = {
|
|
40
|
+
'idempotent': sorted(_IDEMP),
|
|
41
|
+
'biphasic': sorted(_BIPH),
|
|
42
|
+
'involutive': sorted(_INVOL),
|
|
43
|
+
'asymmetric': sorted(_ASYM),
|
|
44
|
+
}
|
|
45
|
+
_CLASS_LOWER: Dict[str, List[str]] = {
|
|
46
|
+
sc: [c.lower() for c in letters]
|
|
47
|
+
for sc, letters in _CLASS_UPPER.items()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_SYM_ID: Dict[str, int] = {
|
|
51
|
+
'idempotent': 1,
|
|
52
|
+
'biphasic': 2,
|
|
53
|
+
'involutive': 3,
|
|
54
|
+
'asymmetric': 4,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ── Per-character functions ───────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def symmetry_class(char: str) -> str:
|
|
60
|
+
"""Return the symmetry class for any alphabetic character.
|
|
61
|
+
|
|
62
|
+
Classification is determined by the uppercase form, so 'f' and 'F'
|
|
63
|
+
both return 'asymmetric'. Non-alpha characters fall back to 'asymmetric'.
|
|
64
|
+
"""
|
|
65
|
+
u = char.upper()
|
|
66
|
+
if u in _IDEMP: return 'idempotent'
|
|
67
|
+
if u in _BIPH: return 'biphasic'
|
|
68
|
+
if u in _INVOL: return 'involutive'
|
|
69
|
+
if u in _ASYM: return 'asymmetric'
|
|
70
|
+
return 'asymmetric'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def sym_energy(char: str) -> float:
|
|
74
|
+
"""SymPhi rest-energy for a single alphabetic character.
|
|
75
|
+
|
|
76
|
+
Uses positional algebra rather than digital root so that energy values
|
|
77
|
+
reflect topological shape and continuous symmetry properties.
|
|
78
|
+
"""
|
|
79
|
+
if not char.isalpha():
|
|
80
|
+
return 0.0
|
|
81
|
+
pos = ord(char.upper()) - ord('A') + 1 # 1–26
|
|
82
|
+
sc = symmetry_class(char)
|
|
83
|
+
if sc == 'idempotent': return float(pos)
|
|
84
|
+
if sc == 'biphasic': return math.sin(pos)
|
|
85
|
+
if sc == 'involutive': return 1.0 / pos
|
|
86
|
+
return float(pos + 1) # asymmetric
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def vectorize(char: str) -> Tuple[float, float, float, float]:
|
|
90
|
+
"""4D vector [pos, type_id, √|E|, sin(pos·π/26)] per the whitepaper."""
|
|
91
|
+
if not char.isalpha():
|
|
92
|
+
return (0.0, 0.0, 0.0, 0.0)
|
|
93
|
+
pos = ord(char.upper()) - ord('A') + 1
|
|
94
|
+
sc = symmetry_class(char)
|
|
95
|
+
tid = float(_SYM_ID.get(sc, 0))
|
|
96
|
+
e = sym_energy(char)
|
|
97
|
+
theta = pos * math.pi / 26.0
|
|
98
|
+
return (float(pos), tid, math.sqrt(abs(e)), math.sin(theta))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def word_sym_energy(word: str) -> float:
|
|
102
|
+
"""Sum of SymPhi rest-energies for all alphabetic characters in *word*."""
|
|
103
|
+
return sum(sym_energy(c) for c in word if c.isalpha())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def word_vector(word: str) -> Tuple[float, float, float, float]:
|
|
107
|
+
"""Sum of 4D character vectors for all alphabetic characters in *word*."""
|
|
108
|
+
v = [0.0, 0.0, 0.0, 0.0]
|
|
109
|
+
for c in word:
|
|
110
|
+
if c.isalpha():
|
|
111
|
+
cv = vectorize(c)
|
|
112
|
+
for i in range(4):
|
|
113
|
+
v[i] += cv[i]
|
|
114
|
+
return (v[0], v[1], v[2], v[3])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── Graph ─────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
class DCG:
|
|
120
|
+
"""Directed Cognition Graph over the 52-letter alphabet.
|
|
121
|
+
|
|
122
|
+
Internal representation: adjacency dict ``{src: {dst: weight}}``.
|
|
123
|
+
All public mutation happens through :func:`build_dcg` / :func:`get_dcg`.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
__slots__ = ('_adj', '_nodes')
|
|
127
|
+
|
|
128
|
+
def __init__(self) -> None:
|
|
129
|
+
self._adj: Dict[str, Dict[str, float]] = {}
|
|
130
|
+
self._nodes: Dict[str, Dict] = {}
|
|
131
|
+
|
|
132
|
+
# ── Internal builders ─────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
def _ensure_node(self, char: str) -> None:
|
|
135
|
+
if char not in self._nodes:
|
|
136
|
+
self._nodes[char] = {
|
|
137
|
+
'class': symmetry_class(char),
|
|
138
|
+
'energy': sym_energy(char),
|
|
139
|
+
'vector': vectorize(char),
|
|
140
|
+
'form': 'upper' if char.isupper() else 'lower',
|
|
141
|
+
}
|
|
142
|
+
self._adj[char] = {}
|
|
143
|
+
|
|
144
|
+
def _add_edge(self, src: str, dst: str, weight: float = 1.0) -> None:
|
|
145
|
+
self._ensure_node(src)
|
|
146
|
+
self._ensure_node(dst)
|
|
147
|
+
if weight < self._adj[src].get(dst, float('inf')):
|
|
148
|
+
self._adj[src][dst] = weight
|
|
149
|
+
|
|
150
|
+
# ── Inspection ────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def node_data(self, char: str) -> Dict:
|
|
153
|
+
"""Return a copy of the metadata dict for *char*."""
|
|
154
|
+
return dict(self._nodes.get(char, {}))
|
|
155
|
+
|
|
156
|
+
def has_edge(self, src: str, dst: str) -> bool:
|
|
157
|
+
return dst in self._adj.get(src, {})
|
|
158
|
+
|
|
159
|
+
def edge_weight(self, src: str, dst: str) -> Optional[float]:
|
|
160
|
+
return self._adj.get(src, {}).get(dst)
|
|
161
|
+
|
|
162
|
+
def successors(self, node: str) -> List[str]:
|
|
163
|
+
return list(self._adj.get(node, {}).keys())
|
|
164
|
+
|
|
165
|
+
def predecessors(self, node: str) -> List[str]:
|
|
166
|
+
return [n for n, dsts in self._adj.items() if node in dsts]
|
|
167
|
+
|
|
168
|
+
def node_count(self) -> int:
|
|
169
|
+
return len(self._nodes)
|
|
170
|
+
|
|
171
|
+
def edge_count(self) -> int:
|
|
172
|
+
return sum(len(dsts) for dsts in self._adj.values())
|
|
173
|
+
|
|
174
|
+
def edge_count_by_type(self) -> Dict[str, int]:
|
|
175
|
+
"""Count edges grouped by source-class → dest-class label."""
|
|
176
|
+
counts: Dict[str, int] = {}
|
|
177
|
+
for src, dsts in self._adj.items():
|
|
178
|
+
sc = self._nodes[src]['class']
|
|
179
|
+
sf = self._nodes[src]['form']
|
|
180
|
+
for dst in dsts:
|
|
181
|
+
dc = self._nodes[dst]['class']
|
|
182
|
+
df = self._nodes[dst]['form']
|
|
183
|
+
if sf != df:
|
|
184
|
+
label = 'dc_ac'
|
|
185
|
+
elif sc == dc:
|
|
186
|
+
label = f'within_{sc}'
|
|
187
|
+
else:
|
|
188
|
+
label = f'{sc}_to_{dc}'
|
|
189
|
+
counts[label] = counts.get(label, 0) + 1
|
|
190
|
+
return counts
|
|
191
|
+
|
|
192
|
+
# ── Algorithms ────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
def shortest_path(self, src: str, dst: str) -> Optional[List[str]]:
|
|
195
|
+
"""Dijkstra shortest path from *src* to *dst*.
|
|
196
|
+
|
|
197
|
+
Returns the ordered node list, or ``None`` if unreachable.
|
|
198
|
+
"""
|
|
199
|
+
if src not in self._nodes or dst not in self._nodes:
|
|
200
|
+
return None
|
|
201
|
+
dist: Dict[str, float] = {n: float('inf') for n in self._nodes}
|
|
202
|
+
prev: Dict[str, Optional[str]] = {n: None for n in self._nodes}
|
|
203
|
+
dist[src] = 0.0
|
|
204
|
+
heap: List[Tuple[float, str]] = [(0.0, src)]
|
|
205
|
+
visited: set = set()
|
|
206
|
+
|
|
207
|
+
while heap:
|
|
208
|
+
d, u = heapq.heappop(heap)
|
|
209
|
+
if u in visited:
|
|
210
|
+
continue
|
|
211
|
+
visited.add(u)
|
|
212
|
+
if u == dst:
|
|
213
|
+
break
|
|
214
|
+
for v, w in self._adj.get(u, {}).items():
|
|
215
|
+
nd = d + w
|
|
216
|
+
if nd < dist[v]:
|
|
217
|
+
dist[v] = nd
|
|
218
|
+
prev[v] = u
|
|
219
|
+
heapq.heappush(heap, (nd, v))
|
|
220
|
+
|
|
221
|
+
if dist[dst] == float('inf'):
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
path: List[str] = []
|
|
225
|
+
cur: Optional[str] = dst
|
|
226
|
+
while cur is not None:
|
|
227
|
+
path.append(cur)
|
|
228
|
+
cur = prev[cur]
|
|
229
|
+
path.reverse()
|
|
230
|
+
return path
|
|
231
|
+
|
|
232
|
+
def shortest_path_cost(self, src: str, dst: str) -> Optional[float]:
|
|
233
|
+
"""Total cost of the shortest path, or ``None`` if unreachable."""
|
|
234
|
+
path = self.shortest_path(src, dst)
|
|
235
|
+
if path is None:
|
|
236
|
+
return None
|
|
237
|
+
cost = 0.0
|
|
238
|
+
for i in range(len(path) - 1):
|
|
239
|
+
cost += self._adj[path[i]][path[i + 1]]
|
|
240
|
+
return cost
|
|
241
|
+
|
|
242
|
+
def word_path_info(self, word: str) -> Dict:
|
|
243
|
+
"""Classify each letter in *word* and check DCG-edge validity per step.
|
|
244
|
+
|
|
245
|
+
Steps for non-alpha characters are skipped so the path covers only
|
|
246
|
+
the meaningful symbolic content.
|
|
247
|
+
"""
|
|
248
|
+
letters = [c for c in word if c.isalpha()]
|
|
249
|
+
steps = []
|
|
250
|
+
for i, ch in enumerate(letters):
|
|
251
|
+
nd = self.node_data(ch)
|
|
252
|
+
step: Dict = {
|
|
253
|
+
'char': ch,
|
|
254
|
+
'class': nd.get('class', '?'),
|
|
255
|
+
'energy': round(nd.get('energy', 0.0), 6),
|
|
256
|
+
'vector': [round(x, 6) for x in nd.get('vector', (0.0, 0.0, 0.0, 0.0))],
|
|
257
|
+
}
|
|
258
|
+
if i > 0:
|
|
259
|
+
prev_ch = letters[i - 1]
|
|
260
|
+
step['edge_from'] = prev_ch
|
|
261
|
+
step['valid_step'] = self.has_edge(prev_ch, ch)
|
|
262
|
+
step['edge_weight'] = self.edge_weight(prev_ch, ch)
|
|
263
|
+
steps.append(step)
|
|
264
|
+
|
|
265
|
+
all_valid = len(steps) <= 1 or all(s.get('valid_step', True) for s in steps)
|
|
266
|
+
return {
|
|
267
|
+
'word': word,
|
|
268
|
+
'steps': steps,
|
|
269
|
+
'total_energy': round(word_sym_energy(word), 6),
|
|
270
|
+
'vector_sum': [round(x, 6) for x in word_vector(word)],
|
|
271
|
+
'is_valid_path': all_valid,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ── Module-level lazy singleton ───────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
_DCG_INSTANCE: Optional[DCG] = None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def get_dcg() -> DCG:
|
|
281
|
+
"""Return the shared DCG instance, building it on first call."""
|
|
282
|
+
global _DCG_INSTANCE
|
|
283
|
+
if _DCG_INSTANCE is None:
|
|
284
|
+
_DCG_INSTANCE = _build_dcg()
|
|
285
|
+
return _DCG_INSTANCE
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _build_dcg() -> DCG:
|
|
289
|
+
"""Construct the full 52-node DCG with all edge types."""
|
|
290
|
+
g = DCG()
|
|
291
|
+
|
|
292
|
+
upper_letters = [chr(i) for i in range(ord('A'), ord('Z') + 1)]
|
|
293
|
+
|
|
294
|
+
# 1. Uppercase ↔ lowercase (DC ↔ AC form conversion)
|
|
295
|
+
for u in upper_letters:
|
|
296
|
+
lo = u.lower()
|
|
297
|
+
g._add_edge(u, lo, 1.0) # DC → AC (forward, cheaper)
|
|
298
|
+
g._add_edge(lo, u, 2.0) # AC → DC (convergence, costlier)
|
|
299
|
+
|
|
300
|
+
# 2. Within-class all-pairs (same symmetry class, same case)
|
|
301
|
+
for case_dict in (_CLASS_UPPER, _CLASS_LOWER):
|
|
302
|
+
for letters in case_dict.values():
|
|
303
|
+
for src in letters:
|
|
304
|
+
for dst in letters:
|
|
305
|
+
if src != dst:
|
|
306
|
+
g._add_edge(src, dst, 1.0)
|
|
307
|
+
|
|
308
|
+
# 3. Cross-class directed cycle (energy-flow circuit)
|
|
309
|
+
# asym → biph → idemp → invol → asym (same-case only)
|
|
310
|
+
_cycle = [
|
|
311
|
+
('asymmetric', 'biphasic'),
|
|
312
|
+
('biphasic', 'idempotent'),
|
|
313
|
+
('idempotent', 'involutive'),
|
|
314
|
+
('involutive', 'asymmetric'),
|
|
315
|
+
]
|
|
316
|
+
for case_dict in (_CLASS_UPPER, _CLASS_LOWER):
|
|
317
|
+
for src_class, dst_class in _cycle:
|
|
318
|
+
for src in case_dict[src_class]:
|
|
319
|
+
for dst in case_dict[dst_class]:
|
|
320
|
+
g._add_edge(src, dst, 2.0)
|
|
321
|
+
|
|
322
|
+
return g
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ── High-level helpers ────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
def homotopy_equivalent(
|
|
328
|
+
word_a: str,
|
|
329
|
+
word_b: str,
|
|
330
|
+
tol: float = 1.0,
|
|
331
|
+
) -> Tuple[bool, Dict]:
|
|
332
|
+
"""Check homotopy equivalence of two words.
|
|
333
|
+
|
|
334
|
+
Two words are homotopy-equivalent when their 4D vector sums are within
|
|
335
|
+
*tol* (Euclidean distance) and their total SymPhi energies differ by at
|
|
336
|
+
most *tol*. This implements the path-homotopy criterion
|
|
337
|
+
H(s,t) = (1−t)p₀(s) + t·p₁(s) from the whitepaper: if the interpolated
|
|
338
|
+
path stays within the tolerance ball, the words are equivalent.
|
|
339
|
+
"""
|
|
340
|
+
ea = word_sym_energy(word_a)
|
|
341
|
+
eb = word_sym_energy(word_b)
|
|
342
|
+
va = word_vector(word_a)
|
|
343
|
+
vb = word_vector(word_b)
|
|
344
|
+
|
|
345
|
+
e_diff = abs(ea - eb)
|
|
346
|
+
v_dist = math.sqrt(sum((a - b) ** 2 for a, b in zip(va, vb)))
|
|
347
|
+
equiv = e_diff <= tol and v_dist <= tol
|
|
348
|
+
|
|
349
|
+
return equiv, {
|
|
350
|
+
'word_a': {'energy': round(ea, 6), 'vector': [round(x, 6) for x in va]},
|
|
351
|
+
'word_b': {'energy': round(eb, 6), 'vector': [round(x, 6) for x in vb]},
|
|
352
|
+
'energy_diff': round(e_diff, 6),
|
|
353
|
+
'vector_distance': round(v_dist, 6),
|
|
354
|
+
'tol': tol,
|
|
355
|
+
'equivalent': equiv,
|
|
356
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Encode ONE GDk9 digital-root rewrite suite into egglog-python.
|
|
2
|
+
|
|
3
|
+
Suite (from gdk9.energy.digital_root + additive energy totals):
|
|
4
|
+
R1 Energy(a) + Energy(b) -> Energy(a + b) (const-fold sum)
|
|
5
|
+
R2 Energy(a) | a>0, a%9==0 -> Energy.root(9) (subsume)
|
|
6
|
+
R3 Energy(a) | a>0, a%9!=0 -> Energy.root(a%9) (subsume)
|
|
7
|
+
R4 Energy(0) -> Energy.root(9) (zero_to_nine)
|
|
8
|
+
|
|
9
|
+
Saturation is two-phase so unbounded integer growth cannot explode the e-graph
|
|
10
|
+
(equating DR-equivalent integers while still adding would diverge).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import List, Sequence
|
|
15
|
+
|
|
16
|
+
from egglog import (
|
|
17
|
+
EGraph,
|
|
18
|
+
Expr,
|
|
19
|
+
eq,
|
|
20
|
+
i64,
|
|
21
|
+
i64Like,
|
|
22
|
+
ne,
|
|
23
|
+
rewrite,
|
|
24
|
+
ruleset,
|
|
25
|
+
vars_,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Energy(Expr):
|
|
30
|
+
def __init__(self, value: i64Like) -> None: ...
|
|
31
|
+
|
|
32
|
+
def __add__(self, other: Energy) -> Energy: ...
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def root(cls, value: i64Like) -> Energy:
|
|
36
|
+
"""Collapsed digital-root form (preferred extract)."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_dr_rules():
|
|
41
|
+
a, b = vars_("a b", i64)
|
|
42
|
+
rs_sum = ruleset(
|
|
43
|
+
rewrite(Energy(a) + Energy(b)).to(Energy(a + b)),
|
|
44
|
+
)
|
|
45
|
+
rs_dr = ruleset(
|
|
46
|
+
rewrite(Energy(a), subsume=True).to(
|
|
47
|
+
Energy.root(9),
|
|
48
|
+
a > 0,
|
|
49
|
+
eq(a % 9).to(i64(0)),
|
|
50
|
+
),
|
|
51
|
+
rewrite(Energy(a), subsume=True).to(
|
|
52
|
+
Energy.root(a % 9),
|
|
53
|
+
a > 0,
|
|
54
|
+
ne(a % 9).to(i64(0)),
|
|
55
|
+
),
|
|
56
|
+
rewrite(Energy(0), subsume=True).to(Energy.root(9)),
|
|
57
|
+
)
|
|
58
|
+
return rs_sum, rs_dr
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_root(extracted: object) -> int | None:
|
|
62
|
+
text = str(extracted)
|
|
63
|
+
# Energy.root(N)
|
|
64
|
+
if "root(" in text:
|
|
65
|
+
inner = text.split("root(", 1)[1].rstrip(")")
|
|
66
|
+
try:
|
|
67
|
+
return int(inner)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return None
|
|
70
|
+
# Energy(N) fallback
|
|
71
|
+
if text.startswith("Energy(") and text.endswith(")"):
|
|
72
|
+
try:
|
|
73
|
+
return int(text[len("Energy(") : -1])
|
|
74
|
+
except ValueError:
|
|
75
|
+
return None
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def saturate_egglog(values: Sequence[int], sum_iters: int = 8, dr_iters: int = 8) -> dict:
|
|
80
|
+
vals = [int(v) for v in values]
|
|
81
|
+
if not vals:
|
|
82
|
+
raise ValueError("need at least one value")
|
|
83
|
+
|
|
84
|
+
egraph = EGraph()
|
|
85
|
+
rs_sum, rs_dr = build_dr_rules()
|
|
86
|
+
|
|
87
|
+
expr = Energy(vals[0])
|
|
88
|
+
for v in vals[1:]:
|
|
89
|
+
expr = expr + Energy(v)
|
|
90
|
+
named = egraph.let("fixture", expr)
|
|
91
|
+
|
|
92
|
+
before = egraph.extract(named)
|
|
93
|
+
egraph.run(rs_sum * sum_iters)
|
|
94
|
+
after_sum = egraph.extract(named)
|
|
95
|
+
egraph.run(rs_dr * dr_iters)
|
|
96
|
+
after_dr = egraph.extract(named)
|
|
97
|
+
best = _parse_root(after_dr)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"engine": "egglog",
|
|
101
|
+
"inputs": vals,
|
|
102
|
+
"before": str(before),
|
|
103
|
+
"after_sum": str(after_sum),
|
|
104
|
+
"after_dr": str(after_dr),
|
|
105
|
+
"best_dr": best,
|
|
106
|
+
"ok": best is not None and 1 <= best <= 9,
|
|
107
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Legacy GDk9 digital-root rewriter (kept as fallback)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Iterable, List, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
from gdk9.energy import digital_root
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def collapse_totals(values: Sequence[int], zero_to_nine: bool = True) -> Tuple[int, int]:
|
|
10
|
+
"""Sum values then digital-root (classic GDk9 path)."""
|
|
11
|
+
total = sum(int(v) for v in values)
|
|
12
|
+
return total, digital_root(total, zero_to_nine=zero_to_nine)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def collapse_each_then_sum(values: Sequence[int], zero_to_nine: bool = True) -> Tuple[List[int], int]:
|
|
16
|
+
"""DR each term, sum, DR again — congruence check path."""
|
|
17
|
+
folded = [digital_root(int(v), zero_to_nine=zero_to_nine) for v in values]
|
|
18
|
+
_total, dr = collapse_totals(folded, zero_to_nine=zero_to_nine)
|
|
19
|
+
return folded, dr
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def saturate_fallback(values: Iterable[int]) -> dict:
|
|
23
|
+
vals = [int(v) for v in values]
|
|
24
|
+
total, dr_direct = collapse_totals(vals)
|
|
25
|
+
folded, dr_via = collapse_each_then_sum(vals)
|
|
26
|
+
return {
|
|
27
|
+
"engine": "gdk9.energy.digital_root (fallback)",
|
|
28
|
+
"inputs": vals,
|
|
29
|
+
"total": total,
|
|
30
|
+
"folded_terms": folded,
|
|
31
|
+
"best_dr": dr_direct,
|
|
32
|
+
"congruence_dr": dr_via,
|
|
33
|
+
"ok": dr_direct == dr_via,
|
|
34
|
+
}
|